Compare commits
83 Commits
3f24fba1ae
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 25cacf0bca | |||
| 513fe78815 | |||
| ebac8042ea | |||
| dd482dd6b0 | |||
| c5453557ae | |||
| b00782b3f6 | |||
| 1b39b2d411 | |||
| 7f30b6be4e | |||
| 0dfbbe5939 | |||
| 32e56b1a0f | |||
| b436a6cade | |||
| dd08655e2c | |||
| 9fe6cb4bf5 | |||
| cf4bc4932a | |||
| dbc909c736 | |||
| 71554da541 | |||
| 8f89503c88 | |||
| 80f04c92ec | |||
| ccbc6f0b67 | |||
| d273731e31 | |||
| 65f8ee360b | |||
| 042c8d1a51 | |||
| a0b0be2b8e | |||
| 8fc1d30578 | |||
| a4d9c28702 | |||
| ccb8db555e | |||
| e70f2337cb | |||
| d59f4ca37f | |||
| 0edc883084 | |||
| d26106eb00 | |||
| 6e14ea65d0 | |||
| 982cbb4cab | |||
| a964fe4d69 | |||
| 6db3caaecb | |||
| fd475d3e63 | |||
| 9b3546b45f | |||
| b8c8c7d7b1 | |||
| 58b8849d8d | |||
| 46d326638b | |||
| cfbe751af7 | |||
| 624a5ff2c0 | |||
| 7d7b979b66 | |||
| 3040b10eed | |||
| 3fb3a8026f | |||
| fccb83c0e5 | |||
| c684523cb8 | |||
| 62c624c88e | |||
| 4481c581e9 | |||
| 22cb2d50d3 | |||
| 51c377964d | |||
| 7e8d824ba6 | |||
| 75ad12efb3 | |||
| 5ffa151f58 | |||
| c9995ef566 | |||
| ad021f2faa | |||
| 9f7160edfc | |||
| 71f1c6f141 | |||
| bb895eccdf | |||
| 9d996acf5f | |||
| 99853b8514 | |||
| efce18e6f5 | |||
| 3dc9bc5b1d | |||
| 7fe415de63 | |||
| 8e42fad8aa | |||
| 7a3c47f1d4 | |||
| dfbb3a9acc | |||
| 19fa8b381c | |||
| b7a9682552 | |||
| f638baec64 | |||
| 1262a8dae4 | |||
| bf8f001a30 | |||
| eac793becb | |||
| 7bdbb7ddff | |||
| c8fd4cf9ca | |||
| 2b190bd4e5 | |||
| 8f2e71849f | |||
| 0207720943 | |||
| 05fba68b3e | |||
| c8c86081d9 | |||
| 983dbb4564 | |||
| 9e237be21e | |||
| 5649a03c47 | |||
| 4a2dcabf58 |
61
.env.example
Normal file
61
.env.example
Normal file
@@ -0,0 +1,61 @@
|
||||
# 应用配置
|
||||
RUN_MODE=development
|
||||
DEBUG_HTTP_DUMP=false
|
||||
UPLOAD_DIR=./data/uploads
|
||||
UPLOAD_PUBLIC_BASE_URL=
|
||||
ARTICLE_UPLOAD_MAX_BYTES=5242880
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME=app
|
||||
DB_USERNAME=dev
|
||||
DB_PASSWORD=dev
|
||||
|
||||
# redis 配置
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
|
||||
# otel 配置
|
||||
OTEL_HOST=127.0.0.1
|
||||
OTEL_PORT=4317
|
||||
OTEL_NAME_SUFFIX=dev
|
||||
|
||||
# 白银节点
|
||||
BAIYIN_CLOUD_URL=
|
||||
BAIYIN_TOKEN_URL=
|
||||
|
||||
# 京东实名
|
||||
IDEN_ACCESS_KEY=
|
||||
IDEN_SECRET_KEY=
|
||||
IDEN_CALLBACK_URL=
|
||||
|
||||
# 支付宝(暂时弃用,但是需要配置)
|
||||
ALIPAY_APP_ID=
|
||||
ALIPAY_APP_PRIVATE_KEY=
|
||||
ALIPAY_PUBLIC_KEY=
|
||||
ALIPAY_API_CERT=
|
||||
|
||||
# 微信支付(暂时弃用,但是需要配置)
|
||||
WECHATPAY_APP_ID=
|
||||
WECHATPAY_MCH_ID=
|
||||
WECHATPAY_MCH_PRIVATE_KEY_SERIAL=
|
||||
WECHATPAY_MCH_PRIVATE_KEY=
|
||||
WECHATPAY_PUBLIC_KEY_ID=
|
||||
WECHATPAY_PUBLIC_KEY=
|
||||
WECHATPAY_API_CERT=
|
||||
WECHATPAY_CALLBACK_URL=
|
||||
|
||||
# 阿里云
|
||||
ALIYUN_ACCESS_KEY=
|
||||
ALIYUN_ACCESS_KEY_SECRET=
|
||||
ALIYUN_SMS_SIGNATURE=
|
||||
ALIYUN_SMS_TEMPLATE_LOGIN=
|
||||
|
||||
# 商福通
|
||||
SFTPAY_ENABLE=
|
||||
SFTPAY_APP_ID=
|
||||
SFTPAY_ROUTE_ID=
|
||||
SFTPAY_APP_PRIVATE_KEY=
|
||||
SFTPAY_PUBLIC_KEY=
|
||||
SFTPAY_RETURN_URL=
|
||||
@@ -2,7 +2,7 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["v*"]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -10,14 +10,12 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,3 +19,5 @@ scripts/*
|
||||
!scripts/env/dev/
|
||||
!scripts/pre/
|
||||
!scripts/sql/
|
||||
|
||||
*/uploads/
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
30
Dockerfile
30
Dockerfile
@@ -1,35 +1,31 @@
|
||||
# 第一阶段:构建
|
||||
FROM golang:1.24.0 AS builder
|
||||
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
FROM golang:1.25.3 AS builder
|
||||
WORKDIR /build
|
||||
|
||||
# 复制Go模块文件
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 编译
|
||||
RUN GOOS=linux GOARCH=amd64 go build -ldflags '-w -s' -o bin/platform_linux_amd64 cmd/main/main.go
|
||||
RUN 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
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# 从构建阶段复制编译好的二进制文件
|
||||
COPY --from=builder /build/bin/platform_linux_amd64 /app/platform
|
||||
|
||||
# 设置可执行权限
|
||||
RUN chmod +x /app/platform
|
||||
|
||||
# 声明暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 启动平台服务
|
||||
CMD ["/app/platform"]
|
||||
CMD ["/app/platform"]
|
||||
|
||||
112
README.md
112
README.md
@@ -1,49 +1,54 @@
|
||||
## TODO
|
||||
|
||||
trade/create 性能问题,缩短事务时间,考虑其他方式实现可靠分布式事务
|
||||
- edge.area_id 可为空,代表节点无固定地区
|
||||
- 后台展示 mac, ip:port,实际地区
|
||||
|
||||
需要确认以下 ID.GenSerial 的分布式并发安全性
|
||||
上传文件平铺到 uploads,不分子文件夹
|
||||
|
||||
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
||||
错误提示增强,展示整链路信息
|
||||
|
||||
端口资源池的 gc 实现
|
||||
交易信息持久化
|
||||
|
||||
标准化生产环境 cors 配置
|
||||
订单关闭问题,在前端关闭窗口后直接调用了全部订单接口,应改成先确认再关闭
|
||||
|
||||
底层调用集成 otel
|
||||
- redis
|
||||
- gorm
|
||||
- 三方接口
|
||||
- 取消订单接口改成只允许管理员调用
|
||||
- 新增关闭订单接口,关闭订单的逻辑是先尝试完成,如果订单未支付则取消订单
|
||||
|
||||
---
|
||||
|
||||
分离 task 的客户端,支持多进程(prefork 必要!)
|
||||
|
||||
调整目录结构:
|
||||
|
||||
```
|
||||
- /core 核心概念
|
||||
- /util 工具函数
|
||||
|
||||
- /models 模型
|
||||
- /queries 数据库层
|
||||
- /clients 三方依赖的客户端实例
|
||||
|
||||
- /services 服务层
|
||||
- /auth 认证相关,特化服务
|
||||
|
||||
- /app 应用相关,初始化日志,环境变量等
|
||||
- /http 协议层,http 服务
|
||||
- /cmd 主函数
|
||||
|
||||
逐层向上依赖
|
||||
cmd 调用 app, http 的初始化函数
|
||||
http 调用 clients 的初始化函数
|
||||
```
|
||||
|
||||
考虑一个方案限制接口请求速率,无侵入更好
|
||||
慢速请求底层调用埋点监控
|
||||
|
||||
冷数据迁移方案
|
||||
|
||||
proxy 网关更新接口可以传输更结构化的数据,直接区分不同类型以加快更新速度
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 新建数据表流程
|
||||
|
||||
1. 创建 model 文件
|
||||
2. 将 model 按照格式添加声明到 `cmd/gen/main.go` 中
|
||||
3. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
|
||||
- 先写 drop table if exists 语句,确保脚本可以幂等执行
|
||||
- 为有必要的字段添加索引
|
||||
- 为数据表及其字段添加注释
|
||||
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
|
||||
4. 调用 `go run ./cmd/gen/main.go` 生成查询文件
|
||||
|
||||
### 更新数据表流程
|
||||
|
||||
1. 更新 model 文件
|
||||
2. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
|
||||
- 先写 drop table if exists 语句,确保脚本可以幂等执行
|
||||
- 为有必要的字段添加索引
|
||||
- 为数据表及其字段添加注释
|
||||
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
|
||||
3. 调用 `go run ./cmd/gen/main.go` 更新查询文件
|
||||
|
||||
### 新增接口或修改接口权限
|
||||
|
||||
1. 在 `web/core/scopes.go` 下声明权限常量。通常格式为 `Model:Action:SubAction`,例如 `User:Create`、`User:Delete`、`User:Update:Password` 等
|
||||
2. 在 `scripts/sql/fill.sql` 文件的权限区域添加或修改权限条目
|
||||
|
||||
## 业务逻辑
|
||||
|
||||
@@ -54,31 +59,22 @@ proxy 网关更新接口可以传输更结构化的数据,直接区分不同
|
||||
3. 异步回调事件,收到支付成功事件后自动完成订单
|
||||
4. 用户退出支付界面,客户端主动发起关闭订单
|
||||
|
||||
### 产品字典表
|
||||
|
||||
| 代码 | 产品 |
|
||||
| ----- | ------------ |
|
||||
| short | 短效动态代理 |
|
||||
| long | 长效动态代理 |
|
||||
|
||||
### 节点分配与存储逻辑
|
||||
|
||||
添加:
|
||||
- 检查用户 ip 是否在白名单内
|
||||
- 取用端口,不够则返回失败
|
||||
- 将分配结果转写成配置发送到网关
|
||||
- 保存通道信息和分配记录,其中通道信息以网关为主体,分配记录以用户为主体
|
||||
- 添加异步任务,当时间结束后释放取用的端口并清空网关配置
|
||||
提取:
|
||||
|
||||
删除:
|
||||
- 如果传入用户信息,检查要删除的连接是否属于该用户
|
||||
- 释放可用端口
|
||||
- redis 脚本中检查,如果端口所属节点已下线则直接忽略
|
||||
- 提交清空配置到网关
|
||||
- 检查用户套餐与白名单
|
||||
- 选中代理
|
||||
- 找到当前可用端口最多的代理
|
||||
- 不考虑分割端口,不够加机器
|
||||
- 获取可用端口
|
||||
- 获取可用节点
|
||||
- 生成批次号,提交到期释放任务
|
||||
- 绑定节点与端口,保存到数据库
|
||||
- 分别提交连接与配置请求
|
||||
|
||||
缩扩容:
|
||||
- 通过调度任务实现缩扩容
|
||||
- 每分钟检查一次全部配置,按代理分组
|
||||
- 获取所有代理后备配置
|
||||
- 后备配置/当前配置
|
||||
- 当比例 < 1.5 或 > 3 时,重新更新为 2 倍
|
||||
释放:
|
||||
|
||||
- 根据批次查出所有端口与相关节点
|
||||
- 分别提交断开与关闭请求
|
||||
- 释放端口
|
||||
|
||||
@@ -35,6 +35,9 @@ func main() {
|
||||
m.Admin{},
|
||||
m.AdminRole{},
|
||||
m.Announcement{},
|
||||
m.Area{},
|
||||
m.Article{},
|
||||
m.ArticleGroup{},
|
||||
m.Bill{},
|
||||
m.Channel{},
|
||||
m.Client{},
|
||||
@@ -51,6 +54,8 @@ func main() {
|
||||
m.LogsUserUsage{},
|
||||
m.Permission{},
|
||||
m.Product{},
|
||||
m.ProductSku{},
|
||||
m.ProductSkuUser{},
|
||||
m.Proxy{},
|
||||
m.Refund{},
|
||||
m.Resource{},
|
||||
@@ -61,6 +66,10 @@ func main() {
|
||||
m.User{},
|
||||
m.UserRole{},
|
||||
m.Whitelist{},
|
||||
m.Inquiry{},
|
||||
m.ProductDiscount{},
|
||||
m.BalanceActivity{},
|
||||
m.CouponUser{},
|
||||
)
|
||||
g.Execute()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: lanhu
|
||||
name: lanhu-platform
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
image: postgres:17.7
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
postgres-migrate:
|
||||
image: postgres:17
|
||||
image: postgres:17.7
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
@@ -31,6 +31,23 @@ services:
|
||||
ports:
|
||||
- "5433:5432"
|
||||
|
||||
asynqmon:
|
||||
image: hibiken/asynqmon:latest
|
||||
environment:
|
||||
- REDIS_ADDR=redis:6379
|
||||
ports:
|
||||
- "9800:8080"
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
gost:
|
||||
image: gogost/gost
|
||||
command: >
|
||||
-api test:test@:9700
|
||||
ports:
|
||||
- "9700:9700"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
6219
docs/api.yaml
6219
docs/api.yaml
File diff suppressed because it is too large
Load Diff
@@ -1,90 +0,0 @@
|
||||
# Docker 部署说明
|
||||
|
||||
本文档说明如何使用 Docker 构建和部署平台服务。
|
||||
|
||||
## 构建镜像
|
||||
|
||||
在项目根目录下执行以下命令构建 Docker 镜像:
|
||||
|
||||
```bash
|
||||
docker build -t platform:latest .
|
||||
```
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
由于涉及敏感的 `.pem` 证书文件,这些文件不包含在代码库或 Docker 镜像中,而是在运行时通过卷挂载的方式提供。
|
||||
|
||||
### 准备证书文件
|
||||
|
||||
1. 在生产服务器上创建一个目录用于存放证书文件,例如:`/path/to/certs/`
|
||||
2. 将必要的证书文件放入此目录:
|
||||
- `pub_key.pem`
|
||||
- `apiclient_key.pem`
|
||||
|
||||
### 运行容器
|
||||
|
||||
使用以下命令运行容器,挂载证书目录:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name platform \
|
||||
-p 8080:8080 \
|
||||
-e APP_PORT=8080 \
|
||||
-v /path/to/certs:/app/certs \
|
||||
platform:latest
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
可以通过环境变量来配置应用程序:
|
||||
|
||||
- `APP_PORT`: 应用监听的端口号(默认: 8080)
|
||||
- 其他应用相关的环境变量可以通过 `-e` 参数添加
|
||||
|
||||
### 证书路径配置
|
||||
|
||||
如果应用程序需要知道证书的路径,可以通过环境变量配置:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name platform \
|
||||
-p 8080:8080 \
|
||||
-e APP_PORT=8080 \
|
||||
-e PUB_KEY_PATH=/app/certs/pub_key.pem \
|
||||
-e APICLIENT_KEY_PATH=/app/certs/apiclient_key.pem \
|
||||
-v /path/to/certs:/app/certs \
|
||||
platform:latest
|
||||
```
|
||||
|
||||
## 使用 Docker Compose
|
||||
|
||||
也可以通过 Docker Compose 进行部署,创建 `docker-compose.yml` 文件:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
platform:
|
||||
image: platform:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- APP_PORT=8080
|
||||
- PUB_KEY_PATH=/app/certs/pub_key.pem
|
||||
- APICLIENT_KEY_PATH=/app/certs/apiclient_key.pem
|
||||
volumes:
|
||||
- /path/to/certs:/app/certs
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
然后执行:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. 确保证书文件的权限设置正确,仅允许必要的用户访问
|
||||
2. 在生产环境中考虑使用 Docker Secrets 或 Kubernetes Secrets 来管理敏感信息
|
||||
3. 定期更新证书和密钥
|
||||
39
go.mod
39
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/go-redsync/redsync/v4 v4.14.1
|
||||
github.com/gofiber/contrib/otelfiber/v2 v2.2.3
|
||||
github.com/gofiber/fiber/v2 v2.52.10
|
||||
github.com/gofiber/template/html/v2 v2.1.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hibiken/asynq v0.25.1
|
||||
github.com/jdcloud-api/jdcloud-sdk-go v1.64.0
|
||||
@@ -22,11 +23,12 @@ require (
|
||||
github.com/smartwalle/alipay/v3 v3.2.28
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/sync v0.18.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/sync v0.20.0
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gen v0.3.27
|
||||
@@ -54,9 +56,11 @@ require (
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/gofiber/template v1.8.3 // indirect
|
||||
github.com/gofiber/utils v1.1.0 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
@@ -83,20 +87,19 @@ require (
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gorm.io/driver/mysql v1.6.0 // indirect
|
||||
gorm.io/hints v1.1.2 // indirect
|
||||
|
||||
86
go.sum
86
go.sum
@@ -115,6 +115,12 @@ github.com/gofiber/contrib/otelfiber/v2 v2.2.3 h1:WKW1XezHFAoohGZwnvC0R8TFJcNkab
|
||||
github.com/gofiber/contrib/otelfiber/v2 v2.2.3/go.mod h1:WdQ1tYbL83IYC6oBaWvKBMVGSAYvSTRuUWTcr0wK1T4=
|
||||
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
|
||||
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
|
||||
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
@@ -148,8 +154,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -271,22 +277,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib v1.38.0 h1:msaHYZ13HfLIbqXsGwZZQBg5zgxwumlZ1mCkXn3E7LM=
|
||||
go.opentelemetry.io/contrib v1.38.0/go.mod h1:4Vp7Az5Dez02V1lCi9OqLvSmSz0lbZu/O2r4XZsqwB0=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -303,8 +309,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -315,8 +321,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -338,8 +344,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -351,8 +357,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -373,8 +379,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -397,8 +403,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -413,35 +419,35 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
37
pkg/env/env.go
vendored
37
pkg/env/env.go
vendored
@@ -18,13 +18,15 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
RunMode = RunModeProd
|
||||
LogLevel = slog.LevelDebug
|
||||
TradeExpire = 15 * 60 // 交易过期时间,单位秒。默认 15 分钟
|
||||
SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时
|
||||
SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天
|
||||
DebugHttpDump = false // 是否打印请求和响应的原始数据
|
||||
DebugExternalChange = true // 是否实际执行外部非幂等接口调用,在开发调试时可以关闭,避免对外部数据产生影响
|
||||
RunMode = RunModeProd
|
||||
LogLevel = slog.LevelDebug
|
||||
TradeExpire = 15 * 60 // 交易过期时间,单位秒。默认 900 秒(15 分钟)
|
||||
SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时
|
||||
SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天
|
||||
DebugHttpDump = false // 是否打印请求和响应的原始数据
|
||||
UploadDir = "./data/uploads"
|
||||
UploadPublicBaseURL = ""
|
||||
ArticleUploadMaxBytes = 5 * 1024 * 1024
|
||||
|
||||
DbHost = "localhost"
|
||||
DbPort = "5432"
|
||||
@@ -36,9 +38,16 @@ var (
|
||||
RedisPort = "6379"
|
||||
RedisPassword = ""
|
||||
|
||||
BaiyinAddr = "http://103.139.212.110:9989"
|
||||
OtelHost string
|
||||
OtelPort string
|
||||
OtelNameSuffix string
|
||||
|
||||
BaiyinCloudUrl string
|
||||
BaiyinTokenUrl string
|
||||
|
||||
GostApiPort = 9700
|
||||
GostApiPathPrefix = ""
|
||||
|
||||
IdenCallbackUrl string
|
||||
IdenAccessKey string
|
||||
IdenSecretKey string
|
||||
@@ -103,7 +112,9 @@ func Init() {
|
||||
errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_EXPIRE", true, nil))
|
||||
errs = append(errs, parse(&SessionRefreshExpire, "SESSION_REFRESH_EXPIRE", true, nil))
|
||||
errs = append(errs, parse(&DebugHttpDump, "DEBUG_HTTP_DUMP", true, nil))
|
||||
errs = append(errs, parse(&DebugExternalChange, "DEBUG_EXTERNAL_CHANGE", true, nil))
|
||||
errs = append(errs, parse(&UploadDir, "UPLOAD_DIR", true, nil))
|
||||
errs = append(errs, parse(&UploadPublicBaseURL, "UPLOAD_PUBLIC_BASE_URL", true, nil))
|
||||
errs = append(errs, parse(&ArticleUploadMaxBytes, "ARTICLE_UPLOAD_MAX_BYTES", true, nil))
|
||||
|
||||
errs = append(errs, parse(&DbHost, "DB_HOST", true, nil))
|
||||
errs = append(errs, parse(&DbPort, "DB_PORT", true, nil))
|
||||
@@ -115,8 +126,14 @@ func Init() {
|
||||
errs = append(errs, parse(&RedisPort, "REDIS_PORT", true, nil))
|
||||
errs = append(errs, parse(&RedisPassword, "REDIS_PASS", true, nil))
|
||||
|
||||
errs = append(errs, parse(&BaiyinAddr, "BAIYIN_ADDR", true, nil))
|
||||
errs = append(errs, parse(&OtelHost, "OTEL_HOST", true, nil))
|
||||
errs = append(errs, parse(&OtelPort, "OTEL_PORT", true, nil))
|
||||
errs = append(errs, parse(&OtelNameSuffix, "OTEL_NAME_SUFFIX", 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(&GostApiPort, "GOST_API_PORT", true, nil))
|
||||
errs = append(errs, parse(&GostApiPathPrefix, "GOST_API_PATH_PREFIX", true, nil))
|
||||
|
||||
errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))
|
||||
errs = append(errs, parse(&IdenAccessKey, "IDEN_ACCESS_KEY", false, nil))
|
||||
|
||||
60
pkg/u/u.go
60
pkg/u/u.go
@@ -17,11 +17,12 @@ func Else[T any](v *T, or T) T {
|
||||
}
|
||||
}
|
||||
|
||||
func ElseTo[A any, B any](a *A, f func(A) B) *B {
|
||||
if a == nil {
|
||||
return nil
|
||||
// 三元表达式
|
||||
func Ternary[T any](condition bool, trueValue T, falseValue T) T {
|
||||
if condition {
|
||||
return trueValue
|
||||
} else {
|
||||
return P(f(*a))
|
||||
return falseValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +53,18 @@ func X[T comparable](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
// N 零值视为 nil
|
||||
func N[T comparable](v *T) *T {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
var zero T
|
||||
if *v == zero {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ====================
|
||||
// 数组
|
||||
// ====================
|
||||
@@ -68,24 +81,15 @@ func Map[T any, R any](src []T, convert func(T) R) []R {
|
||||
// 时间
|
||||
// ====================
|
||||
|
||||
func DateHead(date time.Time) time.Time {
|
||||
var y, m, d = date.Date()
|
||||
return time.Date(y, m, d, 0, 0, 0, 0, date.Location())
|
||||
}
|
||||
|
||||
func DateFoot(date time.Time) time.Time {
|
||||
var y, m, d = date.Date()
|
||||
return time.Date(y, m, d, 23, 59, 59, 999999999, date.Location())
|
||||
func IsSameDate(date1, date2 time.Time) bool {
|
||||
var y1, m1, d1 = date1.Local().Date()
|
||||
var y2, m2, d2 = date2.Local().Date()
|
||||
return y1 == y2 && m1 == m2 && d1 == d2
|
||||
}
|
||||
|
||||
func Today() time.Time {
|
||||
return DateHead(time.Now())
|
||||
}
|
||||
|
||||
func IsSameDate(date1, date2 time.Time) bool {
|
||||
var y1, m1, d1 = date1.Date()
|
||||
var y2, m2, d2 = date2.Date()
|
||||
return y1 == y2 && m1 == m2 && d1 == d2
|
||||
var y, m, d = time.Now().Date()
|
||||
return time.Date(y, m, d, 0, 0, 0, 0, time.Local)
|
||||
}
|
||||
|
||||
func IsToday(date time.Time) bool {
|
||||
@@ -109,3 +113,21 @@ func CombineErrors(errs []error) error {
|
||||
}
|
||||
return combinedErr
|
||||
}
|
||||
|
||||
// ====================
|
||||
// 业务
|
||||
// ====================
|
||||
|
||||
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:]
|
||||
}
|
||||
|
||||
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 repo.lanhuip.com/lanhu/platform:latest .
|
||||
docker build -t repo.lanhuip.com/lanhu/platform:$($args[0]) .
|
||||
|
||||
docker push repo.lanhuip.com/lanhu/platform:latest
|
||||
docker push repo.lanhuip.com/lanhu/platform:$($args[0])
|
||||
258
scripts/sql/fill-area.sql
Normal file
258
scripts/sql/fill-area.sql
Normal file
@@ -0,0 +1,258 @@
|
||||
insert into area
|
||||
(name, level)
|
||||
values
|
||||
('上海',1),
|
||||
('云南',1),
|
||||
('内蒙古',1),
|
||||
('北京',1),
|
||||
('吉林',1),
|
||||
('四川',1),
|
||||
('天津',1),
|
||||
('宁夏',1),
|
||||
('安徽',1),
|
||||
('山东',1),
|
||||
('山西',1),
|
||||
('广东',1),
|
||||
('广西',1),
|
||||
('新疆',1),
|
||||
('江苏',1),
|
||||
('江西',1),
|
||||
('河北',1),
|
||||
('河南',1),
|
||||
('浙江',1),
|
||||
('海南',1),
|
||||
('湖北',1),
|
||||
('湖南',1),
|
||||
('甘肃',1),
|
||||
('福建',1),
|
||||
('贵州',1),
|
||||
('辽宁',1),
|
||||
('重庆',1),
|
||||
('陕西',1),
|
||||
('黑龙江',1)
|
||||
;
|
||||
|
||||
|
||||
insert into area
|
||||
(name, level, parent_id)
|
||||
values
|
||||
('上海', 2, (select id from area where name = '上海')),
|
||||
('昆明', 2, (select id from area where name = '云南')),
|
||||
('包头', 2, (select id from area where name = '内蒙古')),
|
||||
('呼伦贝尔', 2, (select id from area where name = '内蒙古')),
|
||||
('呼和浩特', 2, (select id from area where name = '内蒙古')),
|
||||
('赤峰', 2, (select id from area where name = '内蒙古')),
|
||||
('通辽', 2, (select id from area where name = '内蒙古')),
|
||||
('鄂尔多斯', 2, (select id from area where name = '内蒙古')),
|
||||
('北京', 2, (select id from area where name = '北京')),
|
||||
('四平', 2, (select id from area where name = '吉林')),
|
||||
('延边朝鲜族自治州', 2, (select id from area where name = '吉林')),
|
||||
('松原', 2, (select id from area where name = '吉林')),
|
||||
('白山', 2, (select id from area where name = '吉林')),
|
||||
('通化', 2, (select id from area where name = '吉林')),
|
||||
('长春', 2, (select id from area where name = '吉林')),
|
||||
('乐山', 2, (select id from area where name = '四川')),
|
||||
('内江', 2, (select id from area where name = '四川')),
|
||||
('南充', 2, (select id from area where name = '四川')),
|
||||
('宜宾', 2, (select id from area where name = '四川')),
|
||||
('广元', 2, (select id from area where name = '四川')),
|
||||
('德阳', 2, (select id from area where name = '四川')),
|
||||
('成都', 2, (select id from area where name = '四川')),
|
||||
('攀枝花', 2, (select id from area where name = '四川')),
|
||||
('泸州', 2, (select id from area where name = '四川')),
|
||||
('绵阳', 2, (select id from area where name = '四川')),
|
||||
('自贡', 2, (select id from area where name = '四川')),
|
||||
('达州', 2, (select id from area where name = '四川')),
|
||||
('天津', 2, (select id from area where name = '天津')),
|
||||
('银川', 2, (select id from area where name = '宁夏')),
|
||||
('亳州', 2, (select id from area where name = '安徽')),
|
||||
('六安', 2, (select id from area where name = '安徽')),
|
||||
('合肥', 2, (select id from area where name = '安徽')),
|
||||
('安庆', 2, (select id from area where name = '安徽')),
|
||||
('宣城', 2, (select id from area where name = '安徽')),
|
||||
('宿州', 2, (select id from area where name = '安徽')),
|
||||
('池州', 2, (select id from area where name = '安徽')),
|
||||
('淮北', 2, (select id from area where name = '安徽')),
|
||||
('淮南', 2, (select id from area where name = '安徽')),
|
||||
('滁州', 2, (select id from area where name = '安徽')),
|
||||
('芜湖', 2, (select id from area where name = '安徽')),
|
||||
('蚌埠', 2, (select id from area where name = '安徽')),
|
||||
('铜陵', 2, (select id from area where name = '安徽')),
|
||||
('阜阳', 2, (select id from area where name = '安徽')),
|
||||
('马鞍山', 2, (select id from area where name = '安徽')),
|
||||
('黄山', 2, (select id from area where name = '安徽')),
|
||||
('东营', 2, (select id from area where name = '山东')),
|
||||
('临沂', 2, (select id from area where name = '山东')),
|
||||
('威海', 2, (select id from area where name = '山东')),
|
||||
('德州', 2, (select id from area where name = '山东')),
|
||||
('日照', 2, (select id from area where name = '山东')),
|
||||
('枣庄', 2, (select id from area where name = '山东')),
|
||||
('泰安', 2, (select id from area where name = '山东')),
|
||||
('济南', 2, (select id from area where name = '山东')),
|
||||
('济宁', 2, (select id from area where name = '山东')),
|
||||
('淄博', 2, (select id from area where name = '山东')),
|
||||
('滨州', 2, (select id from area where name = '山东')),
|
||||
('潍坊', 2, (select id from area where name = '山东')),
|
||||
('烟台', 2, (select id from area where name = '山东')),
|
||||
('聊城', 2, (select id from area where name = '山东')),
|
||||
('菏泽', 2, (select id from area where name = '山东')),
|
||||
('青岛', 2, (select id from area where name = '山东')),
|
||||
('临汾', 2, (select id from area where name = '山西')),
|
||||
('吕梁', 2, (select id from area where name = '山西')),
|
||||
('大同', 2, (select id from area where name = '山西')),
|
||||
('太原', 2, (select id from area where name = '山西')),
|
||||
('忻州', 2, (select id from area where name = '山西')),
|
||||
('晋城', 2, (select id from area where name = '山西')),
|
||||
('朔州', 2, (select id from area where name = '山西')),
|
||||
('运城', 2, (select id from area where name = '山西')),
|
||||
('长治', 2, (select id from area where name = '山西')),
|
||||
('阳泉', 2, (select id from area where name = '山西')),
|
||||
('东莞', 2, (select id from area where name = '广东')),
|
||||
('中山', 2, (select id from area where name = '广东')),
|
||||
('云浮', 2, (select id from area where name = '广东')),
|
||||
('佛山', 2, (select id from area where name = '广东')),
|
||||
('广州', 2, (select id from area where name = '广东')),
|
||||
('惠州', 2, (select id from area where name = '广东')),
|
||||
('揭阳', 2, (select id from area where name = '广东')),
|
||||
('梅州', 2, (select id from area where name = '广东')),
|
||||
('汕头', 2, (select id from area where name = '广东')),
|
||||
('汕尾', 2, (select id from area where name = '广东')),
|
||||
('江门', 2, (select id from area where name = '广东')),
|
||||
('河源', 2, (select id from area where name = '广东')),
|
||||
('深圳', 2, (select id from area where name = '广东')),
|
||||
('清远', 2, (select id from area where name = '广东')),
|
||||
('湛江', 2, (select id from area where name = '广东')),
|
||||
('潮州', 2, (select id from area where name = '广东')),
|
||||
('珠海', 2, (select id from area where name = '广东')),
|
||||
('肇庆', 2, (select id from area where name = '广东')),
|
||||
('茂名', 2, (select id from area where name = '广东')),
|
||||
('阳江', 2, (select id from area where name = '广东')),
|
||||
('韶关', 2, (select id from area where name = '广东')),
|
||||
('北海', 2, (select id from area where name = '广西')),
|
||||
('南宁', 2, (select id from area where name = '广西')),
|
||||
('柳州', 2, (select id from area where name = '广西')),
|
||||
('桂林', 2, (select id from area where name = '广西')),
|
||||
('玉林', 2, (select id from area where name = '广西')),
|
||||
('贵港', 2, (select id from area where name = '广西')),
|
||||
('钦州', 2, (select id from area where name = '广西')),
|
||||
('乌鲁木齐', 2, (select id from area where name = '新疆')),
|
||||
('南京', 2, (select id from area where name = '江苏')),
|
||||
('南通', 2, (select id from area where name = '江苏')),
|
||||
('宿迁', 2, (select id from area where name = '江苏')),
|
||||
('常州', 2, (select id from area where name = '江苏')),
|
||||
('徐州', 2, (select id from area where name = '江苏')),
|
||||
('扬州', 2, (select id from area where name = '江苏')),
|
||||
('无锡', 2, (select id from area where name = '江苏')),
|
||||
('泰州', 2, (select id from area where name = '江苏')),
|
||||
('淮安', 2, (select id from area where name = '江苏')),
|
||||
('盐城', 2, (select id from area where name = '江苏')),
|
||||
('苏州', 2, (select id from area where name = '江苏')),
|
||||
('连云港', 2, (select id from area where name = '江苏')),
|
||||
('镇江', 2, (select id from area where name = '江苏')),
|
||||
('上饶', 2, (select id from area where name = '江西')),
|
||||
('九江', 2, (select id from area where name = '江西')),
|
||||
('南昌', 2, (select id from area where name = '江西')),
|
||||
('吉安', 2, (select id from area where name = '江西')),
|
||||
('宜春', 2, (select id from area where name = '江西')),
|
||||
('抚州', 2, (select id from area where name = '江西')),
|
||||
('新余', 2, (select id from area where name = '江西')),
|
||||
('景德镇', 2, (select id from area where name = '江西')),
|
||||
('萍乡', 2, (select id from area where name = '江西')),
|
||||
('赣州', 2, (select id from area where name = '江西')),
|
||||
('鹰潭', 2, (select id from area where name = '江西')),
|
||||
('保定', 2, (select id from area where name = '河北')),
|
||||
('唐山', 2, (select id from area where name = '河北')),
|
||||
('廊坊', 2, (select id from area where name = '河北')),
|
||||
('张家口', 2, (select id from area where name = '河北')),
|
||||
('承德', 2, (select id from area where name = '河北')),
|
||||
('沧州', 2, (select id from area where name = '河北')),
|
||||
('石家庄', 2, (select id from area where name = '河北')),
|
||||
('秦皇岛', 2, (select id from area where name = '河北')),
|
||||
('衡水', 2, (select id from area where name = '河北')),
|
||||
('邢台', 2, (select id from area where name = '河北')),
|
||||
('邯郸', 2, (select id from area where name = '河北')),
|
||||
('信阳', 2, (select id from area where name = '河南')),
|
||||
('南阳', 2, (select id from area where name = '河南')),
|
||||
('周口', 2, (select id from area where name = '河南')),
|
||||
('商丘', 2, (select id from area where name = '河南')),
|
||||
('安阳', 2, (select id from area where name = '河南')),
|
||||
('开封', 2, (select id from area where name = '河南')),
|
||||
('新乡', 2, (select id from area where name = '河南')),
|
||||
('洛阳', 2, (select id from area where name = '河南')),
|
||||
('漯河', 2, (select id from area where name = '河南')),
|
||||
('焦作', 2, (select id from area where name = '河南')),
|
||||
('许昌', 2, (select id from area where name = '河南')),
|
||||
('郑州', 2, (select id from area where name = '河南')),
|
||||
('驻马店', 2, (select id from area where name = '河南')),
|
||||
('鹤壁', 2, (select id from area where name = '河南')),
|
||||
('丽水', 2, (select id from area where name = '浙江')),
|
||||
('台州', 2, (select id from area where name = '浙江')),
|
||||
('嘉兴', 2, (select id from area where name = '浙江')),
|
||||
('宁波', 2, (select id from area where name = '浙江')),
|
||||
('杭州', 2, (select id from area where name = '浙江')),
|
||||
('温州', 2, (select id from area where name = '浙江')),
|
||||
('湖州', 2, (select id from area where name = '浙江')),
|
||||
('绍兴', 2, (select id from area where name = '浙江')),
|
||||
('舟山', 2, (select id from area where name = '浙江')),
|
||||
('衢州', 2, (select id from area where name = '浙江')),
|
||||
('金华', 2, (select id from area where name = '浙江')),
|
||||
('三亚', 2, (select id from area where name = '海南')),
|
||||
('文昌', 2, (select id from area where name = '海南')),
|
||||
('海口', 2, (select id from area where name = '海南')),
|
||||
('咸宁', 2, (select id from area where name = '湖北')),
|
||||
('孝感', 2, (select id from area where name = '湖北')),
|
||||
('宜昌', 2, (select id from area where name = '湖北')),
|
||||
('武汉', 2, (select id from area where name = '湖北')),
|
||||
('荆州', 2, (select id from area where name = '湖北')),
|
||||
('荆门', 2, (select id from area where name = '湖北')),
|
||||
('襄阳', 2, (select id from area where name = '湖北')),
|
||||
('黄冈', 2, (select id from area where name = '湖北')),
|
||||
('黄石', 2, (select id from area where name = '湖北')),
|
||||
('岳阳', 2, (select id from area where name = '湖南')),
|
||||
('株洲', 2, (select id from area where name = '湖南')),
|
||||
('湘潭', 2, (select id from area where name = '湖南')),
|
||||
('衡阳', 2, (select id from area where name = '湖南')),
|
||||
('邵阳', 2, (select id from area where name = '湖南')),
|
||||
('郴州', 2, (select id from area where name = '湖南')),
|
||||
('长沙', 2, (select id from area where name = '湖南')),
|
||||
('兰州', 2, (select id from area where name = '甘肃')),
|
||||
('三明', 2, (select id from area where name = '福建')),
|
||||
('南平', 2, (select id from area where name = '福建')),
|
||||
('厦门', 2, (select id from area where name = '福建')),
|
||||
('宁德', 2, (select id from area where name = '福建')),
|
||||
('泉州', 2, (select id from area where name = '福建')),
|
||||
('福州', 2, (select id from area where name = '福建')),
|
||||
('莆田', 2, (select id from area where name = '福建')),
|
||||
('龙岩', 2, (select id from area where name = '福建')),
|
||||
('六盘水', 2, (select id from area where name = '贵州')),
|
||||
('贵阳', 2, (select id from area where name = '贵州')),
|
||||
('遵义', 2, (select id from area where name = '贵州')),
|
||||
('铜仁', 2, (select id from area where name = '贵州')),
|
||||
('黔东南苗族侗族自治州', 2, (select id from area where name = '贵州')),
|
||||
('大连', 2, (select id from area where name = '辽宁')),
|
||||
('抚顺', 2, (select id from area where name = '辽宁')),
|
||||
('朝阳', 2, (select id from area where name = '辽宁')),
|
||||
('沈阳', 2, (select id from area where name = '辽宁')),
|
||||
('盘锦', 2, (select id from area where name = '辽宁')),
|
||||
('营口', 2, (select id from area where name = '辽宁')),
|
||||
('葫芦岛', 2, (select id from area where name = '辽宁')),
|
||||
('铁岭', 2, (select id from area where name = '辽宁')),
|
||||
('鞍山', 2, (select id from area where name = '辽宁')),
|
||||
('重庆', 2, (select id from area where name = '重庆')),
|
||||
('咸阳', 2, (select id from area where name = '陕西')),
|
||||
('宝鸡', 2, (select id from area where name = '陕西')),
|
||||
('渭南', 2, (select id from area where name = '陕西')),
|
||||
('西安', 2, (select id from area where name = '陕西')),
|
||||
('铜川', 2, (select id from area where name = '陕西')),
|
||||
('七台河', 2, (select id from area where name = '黑龙江')),
|
||||
('伊春', 2, (select id from area where name = '黑龙江')),
|
||||
('佳木斯', 2, (select id from area where name = '黑龙江')),
|
||||
('双鸭山', 2, (select id from area where name = '黑龙江')),
|
||||
('哈尔滨', 2, (select id from area where name = '黑龙江')),
|
||||
('大庆', 2, (select id from area where name = '黑龙江')),
|
||||
('牡丹江', 2, (select id from area where name = '黑龙江')),
|
||||
('绥化', 2, (select id from area where name = '黑龙江')),
|
||||
('鸡西', 2, (select id from area where name = '黑龙江')),
|
||||
('鹤岗', 2, (select id from area where name = '黑龙江')),
|
||||
('黑河', 2, (select id from area where name = '黑龙江')),
|
||||
('齐齐哈尔', 2, (select id from area where name = '黑龙江'))
|
||||
6601
scripts/sql/fill-edge.sql
Normal file
6601
scripts/sql/fill-edge.sql
Normal file
File diff suppressed because it is too large
Load Diff
313
scripts/sql/fill.sql
Normal file
313
scripts/sql/fill.sql
Normal file
@@ -0,0 +1,313 @@
|
||||
-- ====================
|
||||
-- region 客户端
|
||||
-- ====================
|
||||
|
||||
insert into client (type, spec, name, client_id, client_secret, redirect_uri) values (1, 3, 'web', 'web', '$2a$10$Ss12mXQgpYyo1CKIZ3URouDm.Lc2KcYJzsvEK2PTIXlv6fHQht45a', '');
|
||||
insert into client (type, spec, name, client_id, client_secret, redirect_uri) values (1, 3, 'admin', 'admin', '$2a$10$dlfvX5Uf3iVsUWgwlb0Wt.oYsw/OEXgS.Aior3yoT63Ju7ZSsJr/2', '');
|
||||
|
||||
-- ====================
|
||||
-- region 管理员
|
||||
-- ====================
|
||||
|
||||
insert into admin (username, password, name, lock) values ('admin', '', '超级管理员', true);
|
||||
|
||||
-- ====================
|
||||
-- region 产品
|
||||
-- ====================
|
||||
|
||||
delete from product where true;
|
||||
|
||||
insert into product (code, name, description, sort) values ('short', '短效动态', '短效动态', 1);
|
||||
insert into product (code, name, description, sort) values ('long', '长效动态', '长效动态', 2);
|
||||
insert into product (code, name, description, sort) values ('static', '长效静态', '长效静态', 3);
|
||||
|
||||
-- ====================
|
||||
-- region 套餐
|
||||
-- ====================
|
||||
|
||||
delete from product_sku where true;
|
||||
|
||||
insert into product_sku (product_id, code, name, price, price_min, sort) values
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=3&expire=0', '短效动态包量 3 分钟', 10.00, 10.00, 1),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=5&expire=0', '短效动态包量 5 分钟', 10.00, 10.00, 2),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=10&expire=0', '短效动态包量 10 分钟', 10.00, 10.00, 3),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=15&expire=0', '短效动态包量 15 分钟', 10.00, 10.00, 4),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=30&expire=0', '短效动态包量 30 分钟', 10.00, 10.00, 5),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=7', '短效动态包时 3 分钟 7 天', 10.00, 10.00, 6),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=7', '短效动态包时 5 分钟 7 天', 10.00, 10.00, 7),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=7', '短效动态包时 10 分钟 7 天', 10.00, 10.00, 8),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=7', '短效动态包时 15 分钟 7 天', 10.00, 10.00, 9),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=7', '短效动态包时 30 分钟 7 天', 10.00, 10.00, 10),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=15', '短效动态包时 3 分钟 15 天', 10.00, 10.00, 11),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=15', '短效动态包时 5 分钟 15 天', 10.00, 10.00, 12),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=15', '短效动态包时 10 分钟 15 天', 10.00, 10.00, 13),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=15', '短效动态包时 15 分钟 15 天', 10.00, 10.00, 14),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=15', '短效动态包时 30 分钟 15 天', 10.00, 10.00, 15),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=30', '短效动态包时 3 分钟 30 天', 10.00, 10.00, 16),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=30', '短效动态包时 5 分钟 30 天', 10.00, 10.00, 17),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=30', '短效动态包时 10 分钟 30 天', 10.00, 10.00, 18),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=30', '短效动态包时 15 分钟 30 天', 10.00, 10.00, 19),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=30', '短效动态包时 30 分钟 30 天', 10.00, 10.00, 20),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=90', '短效动态包时 3 分钟 90 天', 10.00, 10.00, 21),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=90', '短效动态包时 5 分钟 90 天', 10.00, 10.00, 22),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=90', '短效动态包时 10 分钟 90 天', 10.00, 10.00, 23),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=90', '短效动态包时 15 分钟 90 天', 10.00, 10.00, 24),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=90', '短效动态包时 30 分钟 90 天', 10.00, 10.00, 25),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=180', '短效动态包时 3 分钟 180 天', 10.00, 10.00, 26),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=180', '短效动态包时 5 分钟 180 天', 10.00, 10.00, 27),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=180', '短效动态包时 10 分钟 180 天', 10.00, 10.00, 28),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=180', '短效动态包时 15 分钟 180 天', 10.00, 10.00, 29),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=180', '短效动态包时 30 分钟 180 天', 10.00, 10.00, 30),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=365', '短效动态包时 3 分钟 365 天', 10.00, 10.00, 31),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=365', '短效动态包时 5 分钟 365 天', 10.00, 10.00, 32),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=365', '短效动态包时 10 分钟 365 天', 10.00, 10.00, 33),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=365', '短效动态包时 15 分钟 365 天', 10.00, 10.00, 34),
|
||||
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=365', '短效动态包时 30 分钟 365 天', 10.00, 10.00, 35)
|
||||
;
|
||||
|
||||
insert into product_sku (product_id, code, name, price, price_min, sort) values
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=60&expire=0', '长效动态包量 1 小时', 10.00, 10.00, 1),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=240&expire=0', '长效动态包量 4 小时', 10.00, 10.00, 2),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=480&expire=0', '长效动态包量 8 小时', 10.00, 10.00, 3),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=720&expire=0', '长效动态包量 12 小时', 10.00, 10.00, 4),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=1440&expire=0', '长效动态包量 24 小时', 10.00, 10.00, 5),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=7', '长效动态包时 1 小时 7 天', 10.00, 10.00, 6),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=7', '长效动态包时 4 小时 7 天', 10.00, 10.00, 7),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=7', '长效动态包时 8 小时 7 天', 10.00, 10.00, 8),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=7', '长效动态包时 12 小时 7 天', 10.00, 10.00, 9),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=7', '长效动态包时 24 小时 7 天', 10.00, 10.00, 10),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=15', '长效动态包时 1 小时 15 天', 10.00, 10.00, 11),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=15', '长效动态包时 4 小时 15 天', 10.00, 10.00, 12),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=15', '长效动态包时 8 小时 15 天', 10.00, 10.00, 13),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=15', '长效动态包时 12 小时 15 天', 10.00, 10.00, 14),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=15', '长效动态包时 24 小时 15 天', 10.00, 10.00, 15),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=30', '长效动态包时 1 小时 30 天', 10.00, 10.00, 16),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=30', '长效动态包时 4 小时 30 天', 10.00, 10.00, 17),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=30', '长效动态包时 8 小时 30 天', 10.00, 10.00, 18),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=30', '长效动态包时 12 小时 30 天', 10.00, 10.00, 19),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=30', '长效动态包时 24 小时 30 天', 10.00, 10.00, 20),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=90', '长效动态包时 1 小时 90 天', 10.00, 10.00, 21),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=90', '长效动态包时 4 小时 90 天', 10.00, 10.00, 22),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=90', '长效动态包时 8 小时 90 天', 10.00, 10.00, 23),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=90', '长效动态包时 12 小时 90 天', 10.00, 10.00, 24),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=90', '长效动态包时 24 小时 90 天', 10.00, 10.00, 25),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=180', '长效动态包时 1 小时 180 天', 10.00, 10.00, 26),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=180', '长效动态包时 4 小时 180 天', 10.00, 10.00, 27),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=180', '长效动态包时 8 小时 180 天', 10.00, 10.00, 28),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=180', '长效动态包时 12 小时 180 天', 10.00, 10.00, 29),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=180','长效动态包时 24 小时 180 天', 10.00, 10.00, 30),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=365', '长效动态包时 1 小时 365 天', 10.00, 10.00, 31),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=365', '长效动态包时 4 小时 365 天', 10.00, 10.00, 32),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=365', '长效动态包时 8 小时 365 天', 10.00, 10.00, 33),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=365', '长效动态包时 12 小时 365 天', 10.00, 10.00, 34),
|
||||
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=365','长效动态包时 24 小时 365 天', 10.00, 10.00, 35)
|
||||
;
|
||||
|
||||
-- ====================
|
||||
-- region 权限
|
||||
-- ====================
|
||||
|
||||
delete from permission where true;
|
||||
|
||||
-- --------------------------
|
||||
-- level 1
|
||||
-- --------------------------
|
||||
insert into permission (name, description, sort) values
|
||||
('permission', '权限', 1),
|
||||
('admin_role', '管理员角色', 2),
|
||||
('admin', '管理员', 3),
|
||||
('product', '产品', 4),
|
||||
('product_sku', '产品套餐', 5),
|
||||
('discount', '折扣', 6),
|
||||
('resource', '用户套餐', 7),
|
||||
('user', '用户', 8),
|
||||
('coupon', '优惠券', 9),
|
||||
('batch', '批次', 10),
|
||||
('channel', 'IP', 11),
|
||||
('trade', '交易', 12),
|
||||
('bill', '账单', 13),
|
||||
('balance_activity', '余额变动', 14),
|
||||
('proxy', '代理', 15),
|
||||
('coupon_user', '已发放优惠券', 16),
|
||||
('article', '文档', 17),
|
||||
('article_group', '文档分组', 18);
|
||||
|
||||
-- --------------------------
|
||||
-- level 2
|
||||
-- --------------------------
|
||||
|
||||
-- permission 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'permission' and deleted_at is null), 'permission:read', '读取权限列表', 1),
|
||||
((select id from permission where name = 'permission' and deleted_at is null), 'permission:write', '编辑权限', 2);
|
||||
|
||||
-- admin_role 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:read', '读取管理员角色列表', 1),
|
||||
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:write', '编辑管理员角色', 2);
|
||||
|
||||
-- admin 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'admin' and deleted_at is null), 'admin:read', '读取管理员列表', 1),
|
||||
((select id from permission where name = 'admin' and deleted_at is null), 'admin:write', '编辑管理员', 2);
|
||||
|
||||
-- product 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'product' and deleted_at is null), 'product:read', '读取产品列表', 1),
|
||||
((select id from permission where name = 'product' and deleted_at is null), 'product:write', '编辑产品', 2);
|
||||
|
||||
-- product_sku 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:read', '读取产品套餐列表', 1),
|
||||
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:write', '编辑产品套餐', 2);
|
||||
|
||||
-- discount 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'discount' and deleted_at is null), 'discount:read', '读取折扣列表', 1),
|
||||
((select id from permission where name = 'discount' and deleted_at is null), 'discount:write', '编辑折扣', 2);
|
||||
|
||||
-- resource 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:read', '读取用户套餐列表', 1),
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:write', '编辑用户套餐', 2),
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:short', '短效动态套餐', 3),
|
||||
((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4);
|
||||
|
||||
-- user 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'user' and deleted_at is null), 'user:read', '读取用户列表', 1),
|
||||
((select id from permission where name = 'user' and deleted_at is null), 'user:write', '编辑用户', 2);
|
||||
|
||||
-- coupon 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:read', '读取优惠券列表', 1),
|
||||
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:write', '编辑优惠券', 2);
|
||||
|
||||
-- batch 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'batch' and deleted_at is null), 'batch:read', '读取批次列表', 1),
|
||||
((select id from permission where name = 'batch' and deleted_at is null), 'batch:write', '编辑批次', 2);
|
||||
|
||||
-- channel 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'channel' and deleted_at is null), 'channel:read', '读取 IP 列表', 1),
|
||||
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '编辑 IP', 2);
|
||||
|
||||
-- proxy 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:read', '读取代理列表', 1),
|
||||
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:write', '编辑代理', 2);
|
||||
|
||||
-- trade 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'trade' and deleted_at is null), 'trade:read', '读取交易列表', 1),
|
||||
((select id from permission where name = 'trade' and deleted_at is null), 'trade:write', '编辑交易', 2);
|
||||
|
||||
-- bill 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'bill' and deleted_at is null), 'bill:read', '读取账单列表', 1),
|
||||
((select id from permission where name = 'bill' and deleted_at is null), 'bill:write', '编辑账单', 2);
|
||||
|
||||
-- balance_activity 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'balance_activity' and deleted_at is null), 'balance_activity:read', '读取余额变动列表', 1);
|
||||
|
||||
-- coupon_user 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:read', '读取已发放优惠券列表', 1),
|
||||
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:write', '编辑已发放优惠券', 2);
|
||||
|
||||
-- article 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'article' and deleted_at is null), 'article:read', '读取文档列表', 1),
|
||||
((select id from permission where name = 'article' and deleted_at is null), 'article:write', '编辑文档', 2);
|
||||
|
||||
-- article_group 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'article_group' and deleted_at is null), 'article_group:read', '读取文档分组列表', 1),
|
||||
((select id from permission where name = 'article_group' and deleted_at is null), 'article_group:write', '编辑文档分组', 2);
|
||||
|
||||
-- --------------------------
|
||||
-- level 3
|
||||
-- --------------------------
|
||||
|
||||
-- product_sku:write 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'product_sku:write' and deleted_at is null), 'product_sku:write:status', '更改产品套餐状态', 1);
|
||||
|
||||
-- proxy:write 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'proxy:write' and deleted_at is null), 'proxy:write:status', '更改代理状态', 1);
|
||||
|
||||
-- channel:write 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'channel:write' and deleted_at is null), 'channel:write:clear_expired', '清理过期 IP', 1);
|
||||
|
||||
-- resource:short 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'resource:short' and deleted_at is null), 'resource:short:read', '读取用户短效动态套餐列表', 1);
|
||||
|
||||
-- resource:long 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'resource:long' and deleted_at is null), 'resource:long:read', '读取用户长效动态套餐列表', 1);
|
||||
|
||||
-- user:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'user:read' and deleted_at is null), 'user:read:one', '读取单个用户', 1),
|
||||
((select id from permission where name = 'user:read' and deleted_at is null), 'user:read:not_bind', '读取未绑定管理员的用户列表', 2);
|
||||
|
||||
-- user:write 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:balance', '编辑用户余额', 1),
|
||||
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:bind', '用户认领', 2);
|
||||
|
||||
-- batch:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'batch:read' and deleted_at is null), 'batch:read:of_user', '读取指定用户的批次列表', 1);
|
||||
|
||||
-- channel:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'channel:read' and deleted_at is null), 'channel:read:of_user', '读取指定用户的 IP 列表', 1);
|
||||
|
||||
-- trade:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'trade:read' and deleted_at is null), 'trade:read:of_user', '读取指定用户的交易列表', 1);
|
||||
|
||||
-- bill:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'bill:read' and deleted_at is null), 'bill:read:of_user', '读取指定用户的账单列表', 1);
|
||||
|
||||
-- balance_activity:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'balance_activity:read' and deleted_at is null), 'balance_activity:read:of_user', '读取指定用户的余额变动列表', 1);
|
||||
|
||||
-- coupon:write 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'coupon:write' and deleted_at is null), 'coupon:write:assign', '发放优惠券', 1);
|
||||
|
||||
-- coupon_user:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'coupon_user:read' and deleted_at is null), 'coupon_user:read:of_user', '读取指定用户的已发放优惠券列表', 1);
|
||||
|
||||
-- trade:write 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'trade:write' and deleted_at is null), 'trade:write:complete', '完成交易', 1);
|
||||
|
||||
-- --------------------------
|
||||
-- level 4
|
||||
-- --------------------------
|
||||
|
||||
-- user:write:balance 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'user:write:balance' and deleted_at is null), 'user:write:balance:inc', '增加用户余额', 1),
|
||||
((select id from permission where name = 'user:write:balance' and deleted_at is null), 'user:write:balance:dec', '减少用户余额', 2);
|
||||
|
||||
-- resource:short:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'resource:short:read' and deleted_at is null), 'resource:short:read:of_user', '读取指定用户的短效动态套餐列表', 1);
|
||||
|
||||
-- resource:long:read 子权限
|
||||
insert into permission (parent_id, name, description, sort) values
|
||||
((select id from permission where name = 'resource:long:read' and deleted_at is null), 'resource:long:read:of_user', '读取指定用户的长效动态套餐列表', 1);
|
||||
|
||||
-- endregion
|
||||
@@ -109,74 +109,9 @@ comment on column logs_user_bandwidth.time is '记录时间';
|
||||
-- endregion
|
||||
|
||||
-- ====================
|
||||
-- region 管理员信息
|
||||
-- region 系统信息
|
||||
-- ====================
|
||||
|
||||
-- admin
|
||||
drop table if exists admin cascade;
|
||||
create table admin (
|
||||
id int generated by default as identity primary key,
|
||||
username text not null,
|
||||
password text not null,
|
||||
name text,
|
||||
avatar text,
|
||||
phone text,
|
||||
email text,
|
||||
status int not null default 1,
|
||||
last_login timestamptz,
|
||||
last_login_ip inet,
|
||||
last_login_ua text,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create unique index udx_admin_username on admin (username);
|
||||
create index idx_admin_status on admin (status) where deleted_at is null;
|
||||
create index idx_admin_created_at on admin (created_at) where deleted_at is null;
|
||||
|
||||
-- admin表字段注释
|
||||
comment on table admin is '管理员表';
|
||||
comment on column admin.id is '管理员ID';
|
||||
comment on column admin.username is '用户名';
|
||||
comment on column admin.password is '密码';
|
||||
comment on column admin.name is '真实姓名';
|
||||
comment on column admin.avatar is '头像URL';
|
||||
comment on column admin.phone is '手机号码';
|
||||
comment on column admin.email is '邮箱';
|
||||
comment on column admin.status is '状态:0-禁用,1-正常';
|
||||
comment on column admin.last_login is '最后登录时间';
|
||||
comment on column admin.last_login_ip is '最后登录地址';
|
||||
comment on column admin.last_login_ua is '最后登录代理';
|
||||
comment on column admin.created_at is '创建时间';
|
||||
comment on column admin.updated_at is '更新时间';
|
||||
comment on column admin.deleted_at is '删除时间';
|
||||
|
||||
-- admin_role
|
||||
drop table if exists admin_role cascade;
|
||||
create table admin_role (
|
||||
id int generated by default as identity primary key,
|
||||
name text not null,
|
||||
description text,
|
||||
active bool default true,
|
||||
sort int default 0,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create unique index udx_admin_role_name on admin_role (name) where deleted_at is null;
|
||||
create index idx_admin_role_created_at on admin_role (created_at) where deleted_at is null;
|
||||
|
||||
-- admin_role表字段注释
|
||||
comment on table admin_role is '管理员角色关联表';
|
||||
comment on column admin_role.id is '管理员角色ID';
|
||||
comment on column admin_role.name is '角色名称';
|
||||
comment on column admin_role.description is '角色描述';
|
||||
comment on column admin_role.active is '是否激活';
|
||||
comment on column admin_role.sort is '排序';
|
||||
comment on column admin_role.created_at is '创建时间';
|
||||
comment on column admin_role.updated_at is '更新时间';
|
||||
comment on column admin_role.deleted_at is '删除时间';
|
||||
|
||||
-- announcement
|
||||
drop table if exists announcement cascade;
|
||||
create table announcement (
|
||||
@@ -208,6 +143,168 @@ comment on column announcement.created_at is '创建时间';
|
||||
comment on column announcement.updated_at is '更新时间';
|
||||
comment on column announcement.deleted_at is '删除时间';
|
||||
|
||||
-- article_group
|
||||
drop table if exists article_group cascade;
|
||||
create table article_group (
|
||||
id int generated by default as identity primary key,
|
||||
name text not null,
|
||||
code text not null,
|
||||
sort int not null default 0,
|
||||
status int not null default 1,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create unique index udx_article_group_code on article_group (code) where deleted_at is null;
|
||||
create index idx_article_group_status on article_group (status) where deleted_at is null;
|
||||
create index idx_article_group_sort on article_group (sort) where deleted_at is null;
|
||||
|
||||
-- article_group表字段注释
|
||||
comment on table article_group is '文章分组表';
|
||||
comment on column article_group.id is '分组ID';
|
||||
comment on column article_group.name is '分组名称';
|
||||
comment on column article_group.code is '分组编码';
|
||||
comment on column article_group.sort is '分组排序';
|
||||
comment on column article_group.status is '分组状态:0-禁用,1-正常';
|
||||
comment on column article_group.created_at is '创建时间';
|
||||
comment on column article_group.updated_at is '更新时间';
|
||||
comment on column article_group.deleted_at is '删除时间';
|
||||
|
||||
-- article
|
||||
drop table if exists article cascade;
|
||||
create table article (
|
||||
id int generated by default as identity primary key,
|
||||
group_id int not null,
|
||||
title text not null,
|
||||
content text,
|
||||
sort int not null default 0,
|
||||
status int not null default 1,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create index idx_article_group_id on article (group_id) where deleted_at is null;
|
||||
create index idx_article_status on article (status) where deleted_at is null;
|
||||
create index idx_article_sort on article (sort) where deleted_at is null;
|
||||
|
||||
-- article表字段注释
|
||||
comment on table article is '文章表';
|
||||
comment on column article.id is '文章ID';
|
||||
comment on column article.group_id is '分组ID';
|
||||
comment on column article.title is '文章标题';
|
||||
comment on column article.content is '文章内容';
|
||||
comment on column article.sort is '文章排序';
|
||||
comment on column article.status is '文章状态:0-禁用,1-正常';
|
||||
comment on column article.created_at is '创建时间';
|
||||
comment on column article.updated_at is '更新时间';
|
||||
comment on column article.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 管理员信息
|
||||
-- ====================
|
||||
|
||||
-- admin
|
||||
drop table if exists admin cascade;
|
||||
create table admin (
|
||||
id int generated by default as identity primary key,
|
||||
username text not null,
|
||||
password text not null,
|
||||
name text,
|
||||
avatar text,
|
||||
phone text,
|
||||
email text,
|
||||
status int not null default 1,
|
||||
last_login timestamptz,
|
||||
last_login_ip inet,
|
||||
last_login_ua text,
|
||||
lock bool not null default false,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create unique index udx_admin_username on admin (username) where deleted_at is null;
|
||||
create index idx_admin_status on admin (status) where deleted_at is null;
|
||||
create index idx_admin_created_at on admin (created_at) where deleted_at is null;
|
||||
|
||||
-- admin表字段注释
|
||||
comment on table admin is '管理员表';
|
||||
comment on column admin.id is '管理员ID';
|
||||
comment on column admin.username is '用户名';
|
||||
comment on column admin.password is '密码';
|
||||
comment on column admin.name is '真实姓名';
|
||||
comment on column admin.avatar is '头像URL';
|
||||
comment on column admin.phone is '手机号码';
|
||||
comment on column admin.email is '邮箱';
|
||||
comment on column admin.status is '状态:0-禁用,1-正常';
|
||||
comment on column admin.last_login is '最后登录时间';
|
||||
comment on column admin.last_login_ip is '最后登录地址';
|
||||
comment on column admin.last_login_ua is '最后登录代理';
|
||||
comment on column admin.lock is '是否锁定编辑';
|
||||
comment on column admin.created_at is '创建时间';
|
||||
comment on column admin.updated_at is '更新时间';
|
||||
comment on column admin.deleted_at is '删除时间';
|
||||
|
||||
-- admin_role
|
||||
drop table if exists admin_role cascade;
|
||||
create table admin_role (
|
||||
id int generated by default as identity primary key,
|
||||
name text not null,
|
||||
description text,
|
||||
active bool not null default true,
|
||||
sort int not null default 0,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create unique index udx_admin_role_name on admin_role (name) where deleted_at is null;
|
||||
create index idx_admin_role_created_at on admin_role (created_at) where deleted_at is null;
|
||||
|
||||
-- admin_role表字段注释
|
||||
comment on table admin_role is '管理员角色关联表';
|
||||
comment on column admin_role.id is '管理员角色ID';
|
||||
comment on column admin_role.name is '角色名称';
|
||||
comment on column admin_role.description is '角色描述';
|
||||
comment on column admin_role.active is '是否激活';
|
||||
comment on column admin_role.sort is '排序';
|
||||
comment on column admin_role.created_at is '创建时间';
|
||||
comment on column admin_role.updated_at is '更新时间';
|
||||
comment on column admin_role.deleted_at is '删除时间';
|
||||
|
||||
-- endregion
|
||||
|
||||
-- ====================
|
||||
@@ -219,10 +316,12 @@ drop table if exists "user" cascade;
|
||||
create table "user" (
|
||||
id int generated by default as identity primary key,
|
||||
admin_id int,
|
||||
discount_id int,
|
||||
phone text not null unique,
|
||||
username text,
|
||||
email text,
|
||||
password text,
|
||||
source int default 0,
|
||||
name text,
|
||||
avatar text,
|
||||
status int not null default 1,
|
||||
@@ -240,6 +339,7 @@ create table "user" (
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create index idx_user_admin_id on "user" (admin_id) where deleted_at is null;
|
||||
create index idx_user_discount_id on "user" (discount_id) where deleted_at is null;
|
||||
create unique index udx_user_phone on "user" (phone) where deleted_at is null;
|
||||
create unique index udx_user_username on "user" (username) where deleted_at is null;
|
||||
create unique index udx_user_email on "user" (email) where deleted_at is null;
|
||||
@@ -249,9 +349,11 @@ create index idx_user_created_at on "user" (created_at) where deleted_at is null
|
||||
comment on table "user" is '用户表';
|
||||
comment on column "user".id is '用户ID';
|
||||
comment on column "user".admin_id is '管理员ID';
|
||||
comment on column "user".discount_id is '折扣ID';
|
||||
comment on column "user".password is '用户密码';
|
||||
comment on column "user".username is '用户名';
|
||||
comment on column "user".phone is '手机号码';
|
||||
comment on column "user".source is '用户来源:0-官网注册,1-管理员添加,2-代理商注册,3-代理商添加';
|
||||
comment on column "user".name is '真实姓名';
|
||||
comment on column "user".avatar is '头像URL';
|
||||
comment on column "user".status is '用户状态:0-禁用,1-正常';
|
||||
@@ -389,6 +491,7 @@ create table permission (
|
||||
parent_id int,
|
||||
name text not null,
|
||||
description text,
|
||||
sort int,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
@@ -403,6 +506,7 @@ comment on column permission.id is '权限ID';
|
||||
comment on column permission.parent_id is '父权限ID';
|
||||
comment on column permission.name is '权限名称';
|
||||
comment on column permission.description is '权限描述';
|
||||
comment on column permission.sort is '排序';
|
||||
comment on column permission.created_at is '创建时间';
|
||||
comment on column permission.updated_at is '更新时间';
|
||||
comment on column permission.deleted_at is '删除时间';
|
||||
@@ -410,65 +514,65 @@ comment on column permission.deleted_at is '删除时间';
|
||||
-- link_user_role
|
||||
drop table if exists link_user_role cascade;
|
||||
create table link_user_role (
|
||||
id int generated by default as identity primary key,
|
||||
user_id int not null,
|
||||
role_id int not null
|
||||
id int generated by default as identity primary key,
|
||||
user_id int not null,
|
||||
user_role_id int not null
|
||||
);
|
||||
create index idx_link_user_role_user_id on link_user_role (user_id);
|
||||
create index idx_link_user_role_role_id on link_user_role (role_id);
|
||||
create index idx_link_user_role_role_id on link_user_role (user_role_id);
|
||||
|
||||
-- link_user_role表字段注释
|
||||
comment on table link_user_role is '用户角色关联表';
|
||||
comment on column link_user_role.id is '关联ID';
|
||||
comment on column link_user_role.user_id is '用户ID';
|
||||
comment on column link_user_role.role_id is '角色ID';
|
||||
comment on column link_user_role.user_role_id is '角色ID';
|
||||
|
||||
-- link_admin_role
|
||||
drop table if exists link_admin_role cascade;
|
||||
create table link_admin_role (
|
||||
id int generated by default as identity primary key,
|
||||
admin_id int not null,
|
||||
role_id int not null
|
||||
id int generated by default as identity primary key,
|
||||
admin_id int not null,
|
||||
admin_role_id int not null
|
||||
);
|
||||
create index idx_link_admin_role_admin_id on link_admin_role (admin_id);
|
||||
create index idx_link_admin_role_role_id on link_admin_role (role_id);
|
||||
create index idx_link_admin_role_role_id on link_admin_role (admin_role_id);
|
||||
|
||||
-- link_admin_role表字段注释
|
||||
comment on table link_admin_role is '管理员角色关联表';
|
||||
comment on column link_admin_role.id is '关联ID';
|
||||
comment on column link_admin_role.admin_id is '管理员ID';
|
||||
comment on column link_admin_role.role_id is '角色ID';
|
||||
comment on column link_admin_role.admin_role_id is '角色ID';
|
||||
|
||||
-- link_user_role_permission
|
||||
drop table if exists link_user_role_permission cascade;
|
||||
create table link_user_role_permission (
|
||||
id int generated by default as identity primary key,
|
||||
role_id int not null,
|
||||
user_role_id int not null,
|
||||
permission_id int not null
|
||||
);
|
||||
create index idx_link_user_role_permission_role_id on link_user_role_permission (role_id);
|
||||
create index idx_link_user_role_permission_role_id on link_user_role_permission (user_role_id);
|
||||
create index idx_link_user_role_permission_permission_id on link_user_role_permission (permission_id);
|
||||
|
||||
-- link_user_role_permission表字段注释
|
||||
comment on table link_user_role_permission is '用户角色权限关联表';
|
||||
comment on column link_user_role_permission.id is '关联ID';
|
||||
comment on column link_user_role_permission.role_id is '角色ID';
|
||||
comment on column link_user_role_permission.user_role_id is '角色ID';
|
||||
comment on column link_user_role_permission.permission_id is '权限ID';
|
||||
|
||||
-- link_admin_role_permission
|
||||
drop table if exists link_admin_role_permission cascade;
|
||||
create table link_admin_role_permission (
|
||||
id int generated by default as identity primary key,
|
||||
role_id int not null,
|
||||
admin_role_id int not null,
|
||||
permission_id int not null
|
||||
);
|
||||
create index idx_link_admin_role_permission_role_id on link_admin_role_permission (role_id);
|
||||
create index idx_link_admin_role_permission_role_id on link_admin_role_permission (admin_role_id);
|
||||
create index idx_link_admin_role_permission_permission_id on link_admin_role_permission (permission_id);
|
||||
|
||||
-- link_admin_role_permission表字段注释
|
||||
comment on table link_admin_role_permission is '管理员角色权限关联表';
|
||||
comment on column link_admin_role_permission.id is '关联ID';
|
||||
comment on column link_admin_role_permission.role_id is '角色ID';
|
||||
comment on column link_admin_role_permission.admin_role_id is '角色ID';
|
||||
comment on column link_admin_role_permission.permission_id is '权限ID';
|
||||
|
||||
-- link_client_permission
|
||||
@@ -497,13 +601,15 @@ comment on column link_client_permission.permission_id is '权限ID';
|
||||
drop table if exists proxy cascade;
|
||||
create table proxy (
|
||||
id int generated by default as identity primary key,
|
||||
version int not null,
|
||||
mac text not null,
|
||||
ip inet not null,
|
||||
version int not null,
|
||||
mac text not null,
|
||||
ip inet not null,
|
||||
host text,
|
||||
port int,
|
||||
secret text,
|
||||
type int not null,
|
||||
status int not null,
|
||||
meta jsonb not null,
|
||||
type int not null,
|
||||
status int not null,
|
||||
meta jsonb,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
@@ -516,16 +622,43 @@ create index idx_proxy_created_at on proxy (created_at) where deleted_at is null
|
||||
comment on table proxy is '代理服务表';
|
||||
comment on column proxy.id is '代理服务ID';
|
||||
comment on column proxy.version is '代理服务版本';
|
||||
comment on column proxy.port is '代理服务端口';
|
||||
comment on column proxy.mac is '代理服务名称';
|
||||
comment on column proxy.ip is '代理服务地址';
|
||||
comment on column proxy.type is '代理服务类型:1-自有,2-白银';
|
||||
comment on column proxy.host is '代理服务域名';
|
||||
comment on column proxy.secret is '代理服务密钥';
|
||||
comment on column proxy.type is '代理服务类型:1-自有,2-白银,3-GOST';
|
||||
comment on column proxy.status is '代理服务状态:0-离线,1-在线';
|
||||
comment on column proxy.meta is '代理服务元信息';
|
||||
comment on column proxy.created_at is '创建时间';
|
||||
comment on column proxy.updated_at is '更新时间';
|
||||
comment on column proxy.deleted_at is '删除时间';
|
||||
|
||||
-- area
|
||||
drop table if exists area cascade;
|
||||
create table area (
|
||||
id int generated by default as identity primary key,
|
||||
name text not null,
|
||||
level int not null,
|
||||
parent_id int,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create index idx_area_level on area (level) where deleted_at is null;
|
||||
create index idx_area_parent_id on area (parent_id) where deleted_at is null;
|
||||
create index idx_area_created_at on area (created_at) where deleted_at is null;
|
||||
|
||||
-- area表字段注释
|
||||
comment on table area is '地区表';
|
||||
comment on column area.id is '地区ID';
|
||||
comment on column area.name is '地区名称';
|
||||
comment on column area.level is '地区层级:1-省,2-市';
|
||||
comment on column area.parent_id is '父级地区ID';
|
||||
comment on column area.created_at is '创建时间';
|
||||
comment on column area.updated_at is '更新时间';
|
||||
comment on column area.deleted_at is '删除时间';
|
||||
|
||||
-- edge
|
||||
drop table if exists edge cascade;
|
||||
create table edge (
|
||||
@@ -534,9 +667,9 @@ create table edge (
|
||||
version int not null,
|
||||
mac text not null,
|
||||
ip inet not null,
|
||||
port int,
|
||||
isp int not null,
|
||||
prov text not null,
|
||||
city text not null,
|
||||
area_id int,
|
||||
status int not null default 0,
|
||||
rtt int default 0,
|
||||
loss int default 0,
|
||||
@@ -546,20 +679,19 @@ create table edge (
|
||||
);
|
||||
create unique index udx_edge_mac on edge (mac) where deleted_at is null;
|
||||
create index idx_edge_isp on edge (isp) where deleted_at is null;
|
||||
create index idx_edge_prov on edge (prov) where deleted_at is null;
|
||||
create index idx_edge_city on edge (city) where deleted_at is null;
|
||||
create index idx_edge_area_id on edge (area_id) where deleted_at is null;
|
||||
create index idx_edge_created_at on edge (created_at) where deleted_at is null;
|
||||
|
||||
-- edge表字段注释
|
||||
comment on table edge is '节点表';
|
||||
comment on column edge.id is '节点ID';
|
||||
comment on column edge.type is '节点类型:1-自建';
|
||||
comment on column edge.type is '节点类型:1-自建,2-GOST chain';
|
||||
comment on column edge.version is '节点版本';
|
||||
comment on column edge.mac is '节点 mac 地址';
|
||||
comment on column edge.ip is '节点地址';
|
||||
comment on column edge.mac is '节点 mac 地址或 GOST chain 名称';
|
||||
comment on column edge.ip is '节点地址或 GOST chain addr 的 IP';
|
||||
comment on column edge.port is 'GOST chain addr 的端口';
|
||||
comment on column edge.isp is '运营商:1-电信,2-联通,3-移动';
|
||||
comment on column edge.prov is '省份';
|
||||
comment on column edge.city is '城市';
|
||||
comment on column edge.area_id is '城市地区ID';
|
||||
comment on column edge.status is '节点状态:0-离线,1-正常';
|
||||
comment on column edge.rtt is '最近平均延迟';
|
||||
comment on column edge.loss is '最近丢包率';
|
||||
@@ -600,8 +732,10 @@ create table channel (
|
||||
resource_id int not null,
|
||||
batch_no text not null,
|
||||
proxy_id int not null,
|
||||
host text not null,
|
||||
port int not null,
|
||||
edge_id int,
|
||||
edge_ref text,
|
||||
filter_isp int,
|
||||
filter_prov text,
|
||||
filter_city text,
|
||||
@@ -626,8 +760,10 @@ comment on column channel.user_id is '用户ID';
|
||||
comment on column channel.resource_id is '套餐ID';
|
||||
comment on column channel.batch_no is '批次编号';
|
||||
comment on column channel.proxy_id is '代理ID';
|
||||
comment on column channel.host is '代理主机(快照)';
|
||||
comment on column channel.port is '代理端口';
|
||||
comment on column channel.edge_id is '节点ID(手动配置)';
|
||||
comment on column channel.edge_ref is '外部节点引用,用于索引没有ID的外部非受控节点';
|
||||
comment on column channel.filter_isp is '运营商过滤(自动配置):参考 edge.isp';
|
||||
comment on column channel.filter_prov is '省份过滤(自动配置)';
|
||||
comment on column channel.filter_city is '城市过滤(自动配置)';
|
||||
@@ -675,14 +811,96 @@ comment on column product.created_at is '创建时间';
|
||||
comment on column product.updated_at is '更新时间';
|
||||
comment on column product.deleted_at is '删除时间';
|
||||
|
||||
|
||||
-- product_discount
|
||||
drop table if exists product_discount cascade;
|
||||
create table product_discount (
|
||||
id int generated by default as identity primary key,
|
||||
name text,
|
||||
discount int,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
|
||||
-- product_discount表字段注释
|
||||
comment on table product_discount is '产品折扣表';
|
||||
comment on column product_discount.id is 'ID';
|
||||
comment on column product_discount.name is '折扣名称';
|
||||
comment on column product_discount.discount is '折扣,0 - 100 的整数,表示 xx 折';
|
||||
comment on column product_discount.created_at is '创建时间';
|
||||
comment on column product_discount.updated_at is '更新时间';
|
||||
comment on column product_discount.deleted_at is '删除时间';
|
||||
|
||||
-- product_sku
|
||||
drop table if exists product_sku cascade;
|
||||
create table product_sku (
|
||||
id int generated by default as identity primary key,
|
||||
product_id int not null,
|
||||
discount_id int,
|
||||
code text not null unique,
|
||||
name text not null,
|
||||
price decimal not null,
|
||||
price_min decimal not null,
|
||||
status int not null default 1,
|
||||
sort int not null default 0,
|
||||
count_min int not null default 1,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create index idx_product_sku_product_id on product_sku (product_id) where deleted_at is null;
|
||||
create index idx_product_sku_discount_id on product_sku (discount_id) where deleted_at is null;
|
||||
create unique index idx_product_sku_code on product_sku (code) where deleted_at is null;
|
||||
|
||||
-- product_sku表字段注释
|
||||
comment on table product_sku is '产品SKU表';
|
||||
comment on column product_sku.id is 'SKU ID';
|
||||
comment on column product_sku.product_id is '产品ID';
|
||||
comment on column product_sku.discount_id is '折扣ID';
|
||||
comment on column product_sku.code is 'SKU 代码:格式为 key=value,key=value,...,其中,key:value 是 SKU 的属性,多个属性用逗号分隔';
|
||||
comment on column product_sku.name is 'SKU 可读名称';
|
||||
comment on column product_sku.price_min is '最低价格';
|
||||
comment on column product_sku.status is 'SKU状态:0-禁用,1-正常';
|
||||
comment on column product_sku.sort is '排序';
|
||||
comment on column product_sku.count_min is '最小购买数量';
|
||||
comment on column product_sku.created_at is '创建时间';
|
||||
comment on column product_sku.updated_at is '更新时间';
|
||||
comment on column product_sku.deleted_at is '删除时间';
|
||||
|
||||
-- product_sku_user
|
||||
drop table if exists product_sku_user cascade;
|
||||
create table product_sku_user (
|
||||
id int generated by default as identity primary key,
|
||||
user_id int not null,
|
||||
product_sku_id int not null,
|
||||
discount_id int,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp
|
||||
);
|
||||
create index idx_product_sku_user_user_id on product_sku_user (user_id);
|
||||
create index idx_product_sku_user_product_sku_id on product_sku_user (product_sku_id);
|
||||
create index idx_product_sku_user_discount_id on product_sku_user (discount_id);
|
||||
|
||||
-- product_sku_user表字段注释
|
||||
comment on table product_sku_user is '用户产品SKU表';
|
||||
comment on column product_sku_user.id is 'ID';
|
||||
comment on column product_sku_user.user_id is '用户ID';
|
||||
comment on column product_sku_user.product_sku_id is '产品SKU ID';
|
||||
comment on column product_sku_user.discount_id is '折扣ID';
|
||||
comment on column product_sku_user.created_at is '创建时间';
|
||||
comment on column product_sku_user.updated_at is '更新时间';
|
||||
|
||||
-- resource
|
||||
drop table if exists resource cascade;
|
||||
create table resource (
|
||||
id int generated by default as identity primary key,
|
||||
user_id int not null,
|
||||
resource_no text,
|
||||
active bool not null default true,
|
||||
resource_no text not null,
|
||||
code text,
|
||||
type int not null,
|
||||
active bool not null default true,
|
||||
checkip bool not null default true,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
@@ -690,14 +908,17 @@ create table resource (
|
||||
create unique index udx_resource_resource_no on resource (resource_no);
|
||||
create index idx_resource_user_id on resource (user_id) where deleted_at is null;
|
||||
create index idx_resource_created_at on resource (created_at) where deleted_at is null;
|
||||
create index idx_resource_code on resource (code) where deleted_at is null;
|
||||
|
||||
-- resource表字段注释
|
||||
comment on table resource is '套餐表';
|
||||
comment on column resource.id is '套餐ID';
|
||||
comment on column resource.user_id is '用户ID';
|
||||
comment on column resource.resource_no is '套餐编号';
|
||||
comment on column resource.active is '套餐状态';
|
||||
comment on column resource.code is '产品编码';
|
||||
comment on column resource.type is '套餐类型:1-短效动态,2-长效动态';
|
||||
comment on column resource.active is '套餐状态';
|
||||
comment on column resource.checkip is '提取时是否检查 ip 地址';
|
||||
comment on column resource.created_at is '创建时间';
|
||||
comment on column resource.updated_at is '更新时间';
|
||||
comment on column resource.deleted_at is '删除时间';
|
||||
@@ -707,58 +928,60 @@ drop table if exists resource_short cascade;
|
||||
create table resource_short (
|
||||
id int generated by default as identity primary key,
|
||||
resource_id int not null,
|
||||
type int not null,
|
||||
code text,
|
||||
live int not null,
|
||||
expire timestamptz,
|
||||
quota int,
|
||||
type int not null,
|
||||
quota int not null,
|
||||
expire_at timestamptz,
|
||||
used int not null default 0,
|
||||
daily_limit int not null default 0,
|
||||
daily_used int not null default 0,
|
||||
daily_last timestamptz
|
||||
daily int not null default 0,
|
||||
last_at timestamptz
|
||||
);
|
||||
create index idx_resource_short_resource_id on resource_short (resource_id);
|
||||
create index idx_resource_short_code on resource_short (code);
|
||||
|
||||
-- resource_short表字段注释
|
||||
comment on table resource_short is '短效动态套餐表';
|
||||
comment on column resource_short.id is 'ID';
|
||||
comment on column resource_short.resource_id is '套餐ID';
|
||||
comment on column resource_short.type is '套餐类型:1-包时,2-包量';
|
||||
comment on column resource_short.code is '产品套餐编码';
|
||||
comment on column resource_short.live is '可用时长(秒)';
|
||||
comment on column resource_short.quota is '配额数量';
|
||||
comment on column resource_short.used is '已用数量';
|
||||
comment on column resource_short.expire is '过期时间';
|
||||
comment on column resource_short.daily_limit is '每日限制';
|
||||
comment on column resource_short.daily_used is '今日已用数量';
|
||||
comment on column resource_short.daily_last is '今日最后使用时间';
|
||||
comment on column resource_short.type is '套餐类型:1-包时,2-包量';
|
||||
comment on column resource_short.quota is '每日配额(包时)或总配额(包量)';
|
||||
comment on column resource_short.expire_at is '套餐过期时间,包时模式可用';
|
||||
comment on column resource_short.used is '总用量';
|
||||
comment on column resource_short.daily is '当日用量';
|
||||
comment on column resource_short.last_at is '最后使用时间';
|
||||
|
||||
-- resource_long
|
||||
drop table if exists resource_long cascade;
|
||||
create table resource_long (
|
||||
id int generated by default as identity primary key,
|
||||
resource_id int not null,
|
||||
type int not null,
|
||||
code text,
|
||||
live int not null,
|
||||
expire timestamptz,
|
||||
quota int,
|
||||
type int not null,
|
||||
quota int not null,
|
||||
expire_at timestamptz,
|
||||
used int not null default 0,
|
||||
daily_limit int not null default 0,
|
||||
daily_used int not null default 0,
|
||||
daily_last timestamptz
|
||||
daily int not null default 0,
|
||||
last_at timestamptz
|
||||
);
|
||||
create index idx_resource_long_resource_id on resource_long (resource_id);
|
||||
create index idx_resource_long_code on resource_long (code);
|
||||
|
||||
-- resource_long表字段注释
|
||||
comment on table resource_long is '长效动态套餐表';
|
||||
comment on column resource_long.id is 'ID';
|
||||
comment on column resource_long.resource_id is '套餐ID';
|
||||
comment on column resource_long.code is '产品套餐编码';
|
||||
comment on column resource_long.live is '可用时长(小时)';
|
||||
comment on column resource_long.type is '套餐类型:1-包时,2-包量';
|
||||
comment on column resource_long.live is '可用时长(天)';
|
||||
comment on column resource_long.quota is '配额数量';
|
||||
comment on column resource_long.used is '已用数量';
|
||||
comment on column resource_long.expire is '过期时间';
|
||||
comment on column resource_long.daily_limit is '每日限制';
|
||||
comment on column resource_long.daily_used is '今日已用数量';
|
||||
comment on column resource_long.daily_last is '今日最后使用时间';
|
||||
comment on column resource_long.quota is '每日配额(包时)或总配额(包量)';
|
||||
comment on column resource_long.expire_at is '套餐过期时间,包时模式可用';
|
||||
comment on column resource_long.used is '总用量';
|
||||
comment on column resource_long.daily is '当日用量';
|
||||
comment on column resource_long.last_at is '最后使用时间';
|
||||
|
||||
-- endregion
|
||||
|
||||
@@ -854,10 +1077,12 @@ create table bill (
|
||||
trade_id int,
|
||||
resource_id int,
|
||||
refund_id int,
|
||||
coupon_user_id int,
|
||||
bill_no text not null,
|
||||
info text,
|
||||
type int not null,
|
||||
amount decimal(12, 2) not null default 0,
|
||||
actual decimal(12, 2) not null default 0,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
@@ -867,6 +1092,7 @@ create index idx_bill_user_id on bill (user_id) where deleted_at is null;
|
||||
create index idx_bill_trade_id on bill (trade_id) where deleted_at is null;
|
||||
create index idx_bill_resource_id on bill (resource_id) where deleted_at is null;
|
||||
create index idx_bill_refund_id on bill (refund_id) where deleted_at is null;
|
||||
create index idx_bill_coupon_id on bill (coupon_user_id) where deleted_at is null;
|
||||
create index idx_bill_created_at on bill (created_at) where deleted_at is null;
|
||||
|
||||
-- bill表字段注释
|
||||
@@ -876,47 +1102,102 @@ comment on column bill.user_id is '用户ID';
|
||||
comment on column bill.trade_id is '订单ID';
|
||||
comment on column bill.resource_id is '套餐ID';
|
||||
comment on column bill.refund_id is '退款ID';
|
||||
comment on column bill.coupon_user_id is '优惠券发放ID';
|
||||
comment on column bill.bill_no is '易读账单号';
|
||||
comment on column bill.info is '产品可读信息';
|
||||
comment on column bill.type is '账单类型:1-消费,2-退款,3-充值';
|
||||
comment on column bill.amount is '账单金额';
|
||||
comment on column bill.amount is '应付金额';
|
||||
comment on column bill.actual is '实付金额';
|
||||
comment on column bill.created_at is '创建时间';
|
||||
comment on column bill.updated_at is '更新时间';
|
||||
comment on column bill.deleted_at is '删除时间';
|
||||
|
||||
-- balance_activity 余额变动记录
|
||||
drop table if exists balance_activity cascade;
|
||||
create table balance_activity (
|
||||
id int generated by default as identity primary key,
|
||||
user_id int not null,
|
||||
bill_id int,
|
||||
admin_id int,
|
||||
amount text not null,
|
||||
balance_prev text not null,
|
||||
balance_curr text not null,
|
||||
remark text,
|
||||
created_at timestamptz default current_timestamp
|
||||
);
|
||||
create index idx_balance_activity_user_id on balance_activity (user_id);
|
||||
create index idx_balance_activity_bill_id on balance_activity (bill_id);
|
||||
create index idx_balance_activity_admin_id on balance_activity (admin_id);
|
||||
create index idx_balance_activity_created_at on balance_activity (created_at);
|
||||
|
||||
-- balance_activity表字段注释
|
||||
comment on table balance_activity is '余额变动记录表';
|
||||
comment on column balance_activity.id is '记录ID';
|
||||
comment on column balance_activity.user_id is '用户ID';
|
||||
comment on column balance_activity.bill_id is '账单ID';
|
||||
comment on column balance_activity.admin_id is '管理员ID';
|
||||
comment on column balance_activity.amount is '变动金额';
|
||||
comment on column balance_activity.balance_prev is '变动前余额';
|
||||
comment on column balance_activity.balance_curr is '变动后余额';
|
||||
comment on column balance_activity.remark is '备注';
|
||||
comment on column balance_activity.created_at is '创建时间';
|
||||
|
||||
-- coupon 优惠券
|
||||
drop table if exists coupon cascade;
|
||||
create table coupon (
|
||||
id int generated by default as identity primary key,
|
||||
user_id int,
|
||||
code text not null,
|
||||
remark text,
|
||||
amount decimal(12, 2) not null default 0,
|
||||
min_amount decimal(12, 2) not null default 0,
|
||||
status int not null default 0,
|
||||
expire_at timestamptz,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
id int generated by default as identity primary key,
|
||||
name text not null,
|
||||
amount decimal(12, 2) not null default 0,
|
||||
min_amount decimal(12, 2) not null default 0,
|
||||
count int not null default 0,
|
||||
status int not null default 1,
|
||||
expire_type int not null default 0,
|
||||
expire_at timestamptz,
|
||||
expire_in int,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create index idx_coupon_user_id on coupon (user_id) where deleted_at is null;
|
||||
create index idx_coupon_code on coupon (code) where deleted_at is null;
|
||||
create index idx_coupon_created_at on coupon (created_at) where deleted_at is null;
|
||||
|
||||
-- coupon表字段注释
|
||||
comment on table coupon is '优惠券表';
|
||||
comment on column coupon.id is '优惠券ID';
|
||||
comment on column coupon.user_id is '用户ID';
|
||||
comment on column coupon.code is '优惠券代码';
|
||||
comment on column coupon.remark is '优惠券备注';
|
||||
comment on column coupon.name is '优惠券名称';
|
||||
comment on column coupon.amount is '优惠券金额';
|
||||
comment on column coupon.min_amount is '最低消费金额';
|
||||
comment on column coupon.status is '优惠券状态:0-未使用,1-已使用,2-已过期';
|
||||
comment on column coupon.expire_at is '过期时间';
|
||||
comment on column coupon.count is '优惠券数量';
|
||||
comment on column coupon.status is '优惠券状态:0-禁用,1-正常';
|
||||
comment on column coupon.expire_type is '过期类型:0-不过期,1-固定日期,2-相对日期(从发放时间算起)';
|
||||
comment on column coupon.expire_at is '过期时间,固定日期必填';
|
||||
comment on column coupon.expire_in is '过期时长(天),相对日期必填';
|
||||
comment on column coupon.created_at is '创建时间';
|
||||
comment on column coupon.updated_at is '更新时间';
|
||||
comment on column coupon.deleted_at is '删除时间';
|
||||
|
||||
-- coupon_user 优惠券发放
|
||||
drop table if exists coupon_user cascade;
|
||||
create table coupon_user (
|
||||
id int generated by default as identity primary key,
|
||||
coupon_id int not null,
|
||||
user_id int not null,
|
||||
status int not null default 0,
|
||||
expire_at timestamptz,
|
||||
used_at timestamptz,
|
||||
created_at timestamptz default current_timestamp
|
||||
);
|
||||
create index idx_coupon_user_coupon_id on coupon_user (coupon_id);
|
||||
create index idx_coupon_user_user_id on coupon_user (user_id);
|
||||
|
||||
-- coupon_user表字段注释
|
||||
comment on table coupon_user is '优惠券发放表';
|
||||
comment on column coupon_user.id is '记录ID';
|
||||
comment on column coupon_user.coupon_id is '优惠券ID';
|
||||
comment on column coupon_user.user_id is '用户ID';
|
||||
comment on column coupon_user.status is '使用状态:0-未使用,1-已使用,2-已禁用';
|
||||
comment on column coupon_user.expire_at is '过期时间';
|
||||
comment on column coupon_user.used_at is '使用时间';
|
||||
comment on column coupon_user.created_at is '创建时间';
|
||||
|
||||
-- endregion
|
||||
|
||||
-- ====================
|
||||
@@ -926,6 +1207,8 @@ comment on column coupon.deleted_at is '删除时间';
|
||||
-- user表外键
|
||||
alter table "user"
|
||||
add constraint fk_user_admin_id foreign key (admin_id) references admin (id) on delete set null;
|
||||
alter table "user"
|
||||
add constraint fk_user_discount_id foreign key (discount_id) references product_discount (id) on delete set null;
|
||||
|
||||
-- session表外键
|
||||
alter table session
|
||||
@@ -941,23 +1224,23 @@ alter table permission
|
||||
alter table link_user_role
|
||||
add constraint fk_link_user_role_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||
alter table link_user_role
|
||||
add constraint fk_link_user_role_role_id foreign key (role_id) references user_role (id) on delete cascade;
|
||||
add constraint fk_link_user_role_role_id foreign key (user_role_id) references user_role (id) on delete cascade;
|
||||
|
||||
-- link_admin_role表外键
|
||||
alter table link_admin_role
|
||||
add constraint fk_link_admin_role_admin_id foreign key (admin_id) references admin (id) on delete cascade;
|
||||
alter table link_admin_role
|
||||
add constraint fk_link_admin_role_role_id foreign key (role_id) references admin_role (id) on delete cascade;
|
||||
add constraint fk_link_admin_role_role_id foreign key (admin_role_id) references admin_role (id) on delete cascade;
|
||||
|
||||
-- link_user_role_permission表外键
|
||||
alter table link_user_role_permission
|
||||
add constraint fk_link_user_role_permission_role_id foreign key (role_id) references user_role (id) on delete cascade;
|
||||
add constraint fk_link_user_role_permission_role_id foreign key (user_role_id) references user_role (id) on delete cascade;
|
||||
alter table link_user_role_permission
|
||||
add constraint fk_link_user_role_permission_permission_id foreign key (permission_id) references permission (id) on delete cascade;
|
||||
|
||||
-- link_admin_role_permission表外键
|
||||
alter table link_admin_role_permission
|
||||
add constraint fk_link_admin_role_permission_role_id foreign key (role_id) references admin_role (id) on delete cascade;
|
||||
add constraint fk_link_admin_role_permission_role_id foreign key (admin_role_id) references admin_role (id) on delete cascade;
|
||||
alter table link_admin_role_permission
|
||||
add constraint fk_link_admin_role_permission_permission_id foreign key (permission_id) references permission (id) on delete cascade;
|
||||
|
||||
@@ -976,6 +1259,10 @@ alter table channel
|
||||
add constraint fk_channel_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||
alter table channel
|
||||
add constraint fk_channel_proxy_id foreign key (proxy_id) references proxy (id) on delete set null;
|
||||
alter table area
|
||||
add constraint fk_area_parent_id foreign key (parent_id) references area (id) on delete set null;
|
||||
alter table edge
|
||||
add constraint fk_edge_area_id foreign key (area_id) references area (id) on delete restrict;
|
||||
alter table channel
|
||||
add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null;
|
||||
alter table channel
|
||||
@@ -984,14 +1271,20 @@ alter table channel
|
||||
-- resource表外键
|
||||
alter table resource
|
||||
add constraint fk_resource_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||
alter table resource
|
||||
add constraint fk_product_code foreign key (code) references product (code) on update cascade on delete restrict;
|
||||
|
||||
-- resource_short表外键
|
||||
alter table resource_short
|
||||
add constraint fk_resource_short_resource_id foreign key (resource_id) references resource (id) on delete cascade;
|
||||
alter table resource_short
|
||||
add constraint fk_resource_short_code foreign key (code) references product_sku (code) on update cascade on delete restrict;
|
||||
|
||||
-- resource_long表外键
|
||||
alter table resource_long
|
||||
add constraint fk_resource_long_resource_id foreign key (resource_id) references resource (id) on delete cascade;
|
||||
alter table resource_long
|
||||
add constraint fk_resource_long_code foreign key (code) references product_sku (code) on update cascade on delete restrict;
|
||||
|
||||
-- trade表外键
|
||||
alter table trade
|
||||
@@ -1012,22 +1305,37 @@ alter table bill
|
||||
add constraint fk_bill_resource_id foreign key (resource_id) references resource (id) on delete set null;
|
||||
alter table bill
|
||||
add constraint fk_bill_refund_id foreign key (refund_id) references refund (id) on delete set null;
|
||||
alter table bill
|
||||
add constraint fk_bill_coupon_id foreign key (coupon_user_id) references coupon_user (id) on delete set null;
|
||||
|
||||
-- coupon表外键
|
||||
alter table coupon
|
||||
add constraint fk_coupon_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||
|
||||
-- endregion
|
||||
|
||||
-- ====================
|
||||
-- region 填充数据
|
||||
-- ====================
|
||||
|
||||
insert into client (
|
||||
client_id, client_secret, redirect_uri, spec, name, type
|
||||
)
|
||||
values (
|
||||
'web', '$2a$10$Ss12mXQgpYyo1CKIZ3URouDm.Lc2KcYJzsvEK2PTIXlv6fHQht45a', '', 3, 'web', 1
|
||||
);
|
||||
-- coupon_user表外键
|
||||
alter table coupon_user
|
||||
add constraint fk_coupon_user_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||
alter table coupon_user
|
||||
add constraint fk_coupon_user_coupon_id foreign key (coupon_id) references coupon (id) on delete cascade;
|
||||
|
||||
-- article表外键
|
||||
alter table article
|
||||
add constraint fk_article_group_id foreign key (group_id) references article_group (id) on delete restrict;
|
||||
|
||||
-- product_sku表外键
|
||||
alter table product_sku
|
||||
add constraint fk_product_sku_product_id foreign key (product_id) references product (id) on delete cascade;
|
||||
alter table product_sku
|
||||
add constraint fk_product_sku_discount_id foreign key (discount_id) references product_discount (id) on delete restrict;
|
||||
|
||||
-- product_sku_user表外键
|
||||
alter table product_sku_user
|
||||
add constraint fk_product_sku_user_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||
alter table product_sku_user
|
||||
add constraint fk_product_sku_user_product_sku_id foreign key (product_sku_id) references product_sku (id) on delete cascade;
|
||||
alter table product_sku_user
|
||||
add constraint fk_product_sku_user_discount_id foreign key (discount_id) references product_discount (id) on delete restrict;
|
||||
|
||||
--balance_activity表外键
|
||||
alter table balance_activity
|
||||
add constraint fk_balance_activity_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||
alter table balance_activity
|
||||
add constraint fk_balance_activity_bill_id foreign key (bill_id) references bill (id) on delete set null;
|
||||
|
||||
-- endregion
|
||||
|
||||
81
scripts/sql/update.sql
Normal file
81
scripts/sql/update.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- ====================
|
||||
-- region 套餐更新
|
||||
-- ====================
|
||||
|
||||
-- --------------------------
|
||||
-- 短效动态
|
||||
-- --------------------------
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 3 分钟', price = 10.00, price_min = 10.00, sort = 1 where code = 'mode=quota&live=3&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 5 分钟', price = 10.00, price_min = 10.00, sort = 2 where code = 'mode=quota&live=5&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 10 分钟', price = 10.00, price_min = 10.00, sort = 3 where code = 'mode=quota&live=10&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 15 分钟', price = 10.00, price_min = 10.00, sort = 4 where code = 'mode=quota&live=15&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包量 30 分钟', price = 10.00, price_min = 10.00, sort = 5 where code = 'mode=quota&live=30&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 7 天', price = 10.00, price_min = 10.00, sort = 6 where code = 'mode=time&live=3&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 7 天', price = 10.00, price_min = 10.00, sort = 7 where code = 'mode=time&live=5&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 7 天', price = 10.00, price_min = 10.00, sort = 8 where code = 'mode=time&live=10&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 7 天', price = 10.00, price_min = 10.00, sort = 9 where code = 'mode=time&live=15&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 7 天', price = 10.00, price_min = 10.00, sort = 10 where code = 'mode=time&live=30&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 15 天', price = 10.00, price_min = 10.00, sort = 11 where code = 'mode=time&live=3&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 15 天', price = 10.00, price_min = 10.00, sort = 12 where code = 'mode=time&live=5&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 15 天', price = 10.00, price_min = 10.00, sort = 13 where code = 'mode=time&live=10&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 15 天', price = 10.00, price_min = 10.00, sort = 14 where code = 'mode=time&live=15&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 15 天', price = 10.00, price_min = 10.00, sort = 15 where code = 'mode=time&live=30&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 30 天', price = 10.00, price_min = 10.00, sort = 16 where code = 'mode=time&live=3&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 30 天', price = 10.00, price_min = 10.00, sort = 17 where code = 'mode=time&live=5&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 30 天', price = 10.00, price_min = 10.00, sort = 18 where code = 'mode=time&live=10&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 30 天', price = 10.00, price_min = 10.00, sort = 19 where code = 'mode=time&live=15&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 30 天', price = 10.00, price_min = 10.00, sort = 20 where code = 'mode=time&live=30&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 90 天', price = 10.00, price_min = 10.00, sort = 21 where code = 'mode=time&live=3&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 90 天', price = 10.00, price_min = 10.00, sort = 22 where code = 'mode=time&live=5&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 90 天', price = 10.00, price_min = 10.00, sort = 23 where code = 'mode=time&live=10&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 90 天', price = 10.00, price_min = 10.00, sort = 24 where code = 'mode=time&live=15&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 90 天', price = 10.00, price_min = 10.00, sort = 25 where code = 'mode=time&live=30&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 180 天', price = 10.00, price_min = 10.00, sort = 26 where code = 'mode=time&live=3&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 180 天', price = 10.00, price_min = 10.00, sort = 27 where code = 'mode=time&live=5&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 180 天', price = 10.00, price_min = 10.00, sort = 28 where code = 'mode=time&live=10&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 180 天', price = 10.00, price_min = 10.00, sort = 29 where code = 'mode=time&live=15&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 180 天', price = 10.00, price_min = 10.00, sort = 30 where code = 'mode=time&live=30&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 3 分钟 365 天', price = 10.00, price_min = 10.00, sort = 31 where code = 'mode=time&live=3&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 5 分钟 365 天', price = 10.00, price_min = 10.00, sort = 32 where code = 'mode=time&live=5&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 10 分钟 365 天', price = 10.00, price_min = 10.00, sort = 33 where code = 'mode=time&live=10&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 15 分钟 365 天', price = 10.00, price_min = 10.00, sort = 34 where code = 'mode=time&live=15&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'short' and deleted_at is null), name = '短效动态包时 30 分钟 365 天', price = 10.00, price_min = 10.00, sort = 35 where code = 'mode=time&live=30&expire=365' and deleted_at is null;
|
||||
|
||||
-- --------------------------
|
||||
-- 长效动态
|
||||
-- --------------------------
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 1 小时', price = 10.00, price_min = 10.00, sort = 1 where code = 'mode=quota&live=60&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 4 小时', price = 10.00, price_min = 10.00, sort = 2 where code = 'mode=quota&live=240&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 8 小时', price = 10.00, price_min = 10.00, sort = 3 where code = 'mode=quota&live=480&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 12 小时', price = 10.00, price_min = 10.00, sort = 4 where code = 'mode=quota&live=720&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包量 24 小时', price = 10.00, price_min = 10.00, sort = 5 where code = 'mode=quota&live=1440&expire=0' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 7 天', price = 10.00, price_min = 10.00, sort = 6 where code = 'mode=time&live=60&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 7 天', price = 10.00, price_min = 10.00, sort = 7 where code = 'mode=time&live=240&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 7 天', price = 10.00, price_min = 10.00, sort = 8 where code = 'mode=time&live=480&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 7 天', price = 10.00, price_min = 10.00, sort = 9 where code = 'mode=time&live=720&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 7 天', price = 10.00, price_min = 10.00, sort = 10 where code = 'mode=time&live=1440&expire=7' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 15 天', price = 10.00, price_min = 10.00, sort = 11 where code = 'mode=time&live=60&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 15 天', price = 10.00, price_min = 10.00, sort = 12 where code = 'mode=time&live=240&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 15 天', price = 10.00, price_min = 10.00, sort = 13 where code = 'mode=time&live=480&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 15 天', price = 10.00, price_min = 10.00, sort = 14 where code = 'mode=time&live=720&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 15 天', price = 10.00, price_min = 10.00, sort = 15 where code = 'mode=time&live=1440&expire=15' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 30 天', price = 10.00, price_min = 10.00, sort = 16 where code = 'mode=time&live=60&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 30 天', price = 10.00, price_min = 10.00, sort = 17 where code = 'mode=time&live=240&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 30 天', price = 10.00, price_min = 10.00, sort = 18 where code = 'mode=time&live=480&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 30 天', price = 10.00, price_min = 10.00, sort = 19 where code = 'mode=time&live=720&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 30 天', price = 10.00, price_min = 10.00, sort = 20 where code = 'mode=time&live=1440&expire=30' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 90 天', price = 10.00, price_min = 10.00, sort = 21 where code = 'mode=time&live=60&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 90 天', price = 10.00, price_min = 10.00, sort = 22 where code = 'mode=time&live=240&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 90 天', price = 10.00, price_min = 10.00, sort = 23 where code = 'mode=time&live=480&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 90 天', price = 10.00, price_min = 10.00, sort = 24 where code = 'mode=time&live=720&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 90 天', price = 10.00, price_min = 10.00, sort = 25 where code = 'mode=time&live=1440&expire=90' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 180 天', price = 10.00, price_min = 10.00, sort = 26 where code = 'mode=time&live=60&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 180 天', price = 10.00, price_min = 10.00, sort = 27 where code = 'mode=time&live=240&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 180 天', price = 10.00, price_min = 10.00, sort = 28 where code = 'mode=time&live=480&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 180 天', price = 10.00, price_min = 10.00, sort = 29 where code = 'mode=time&live=720&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 180 天', price = 10.00, price_min = 10.00, sort = 30 where code = 'mode=time&live=1440&expire=180' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 1 小时 365 天', price = 10.00, price_min = 10.00, sort = 31 where code = 'mode=time&live=60&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 4 小时 365 天', price = 10.00, price_min = 10.00, sort = 32 where code = 'mode=time&live=240&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 8 小时 365 天', price = 10.00, price_min = 10.00, sort = 33 where code = 'mode=time&live=480&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 12 小时 365 天', price = 10.00, price_min = 10.00, sort = 34 where code = 'mode=time&live=720&expire=365' and deleted_at is null;
|
||||
update product_sku set product_id = (select id from product where code = 'long' and deleted_at is null), name = '长效动态包时 24 小时 365 天', price = 10.00, price_min = 10.00, sort = 35 where code = 'mode=time&live=1440&expire=365' and deleted_at is null;
|
||||
176
web/auth/account.go
Normal file
176
web/auth/account.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"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.
|
||||
Preload(q.Client.Permissions).
|
||||
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("客户端密钥错误")
|
||||
}
|
||||
}
|
||||
|
||||
// 组织授权信息(一次性请求)
|
||||
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,
|
||||
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, s.VerifierSmsPurposeLogin)
|
||||
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
|
||||
}
|
||||
|
||||
func adminScopes(admin *m.Admin) ([]string, error) {
|
||||
var scopes []struct{ Name string }
|
||||
err := q.Admin.
|
||||
LeftJoin(q.LinkAdminRole, q.LinkAdminRole.AdminID.EqCol(q.Admin.ID)).
|
||||
LeftJoin(q.LinkAdminRolePermission, q.LinkAdminRolePermission.RoleID.EqCol(q.LinkAdminRole.RoleID)).
|
||||
LeftJoin(q.Permission, q.Permission.ID.EqCol(q.LinkAdminRolePermission.PermissionID)).
|
||||
Where(q.Admin.ID.Eq(admin.ID)).
|
||||
Select(q.Permission.Name).
|
||||
Scan(&scopes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopeNames := make([]string, 0, len(scopes))
|
||||
for _, scope := range scopes {
|
||||
if scope.Name == "" {
|
||||
continue
|
||||
}
|
||||
scopeNames = append(scopeNames, scope.Name)
|
||||
}
|
||||
return scopeNames, nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
m "platform/web/models"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
@@ -12,7 +13,6 @@ type AuthCtx struct {
|
||||
Client *m.Client `json:"client,omitempty"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Session *m.Session `json:"session,omitempty"`
|
||||
smap map[string]struct{}
|
||||
}
|
||||
|
||||
func (a *AuthCtx) PermitUser(scopes ...string) (*AuthCtx, error) {
|
||||
@@ -68,14 +68,11 @@ func (a *AuthCtx) checkScopes(scopes ...string) bool {
|
||||
if len(scopes) == 0 || len(a.Scopes) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(a.smap) == 0 && len(a.Scopes) > 0 {
|
||||
for _, scope := range scopes {
|
||||
a.smap[scope] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, scope := range scopes {
|
||||
if _, ok := a.smap[scope]; ok {
|
||||
return true
|
||||
for _, prefix := range a.Scopes {
|
||||
if strings.HasPrefix(scope, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
"platform/web/globals/orm"
|
||||
m "platform/web/models"
|
||||
@@ -22,67 +20,52 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GrantType string
|
||||
// AuthorizeGet 授权端点
|
||||
func AuthorizeGet(ctx *fiber.Ctx) error {
|
||||
|
||||
const (
|
||||
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
|
||||
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
|
||||
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
|
||||
GrantPassword = GrantType("password") // 密码模式(私有扩展)
|
||||
)
|
||||
// 检查请求
|
||||
req := new(AuthorizeGetReq)
|
||||
if err := g.Validator.ParseQuery(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type PasswordGrantType string
|
||||
// 检查客户端
|
||||
client, err := authClient(req.ClientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
GrantPasswordSecret = PasswordGrantType("password") // 账号密码
|
||||
GrantPasswordPhone = PasswordGrantType("phone_code") // 手机验证码
|
||||
GrantPasswordEmail = PasswordGrantType("email_code") // 邮箱验证码
|
||||
)
|
||||
if client.RedirectURI == nil || *client.RedirectURI != req.RedirectURI {
|
||||
return errors.New("客户端重定向URI错误")
|
||||
}
|
||||
|
||||
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
|
||||
// todo 检查 scope
|
||||
|
||||
// 授权确认页面
|
||||
return nil
|
||||
}
|
||||
|
||||
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 AuthorizeGetReq struct {
|
||||
ResponseType string `json:"response_type" validate:"eq=code"`
|
||||
ClientID string `json:"client_id" validate:"required"`
|
||||
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 {
|
||||
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
||||
}
|
||||
|
||||
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"`
|
||||
type AuthorizePostReq struct {
|
||||
Accept bool `json:"accept"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// Token 令牌端点
|
||||
func Token(c *fiber.Ctx) error {
|
||||
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) {
|
||||
|
||||
// 检查 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))
|
||||
}
|
||||
|
||||
err = SaveSession(session)
|
||||
err = SaveSession(q.Q, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,6 +288,7 @@ func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.
|
||||
|
||||
func authClientCredential(c *fiber.Ctx, auth *AuthCtx, _ *TokenReq, now time.Time) (*m.Session, error) {
|
||||
// todo 检查 scope
|
||||
scopes := strings.Join(auth.Scopes, " ")
|
||||
|
||||
// 生成会话
|
||||
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
|
||||
@@ -246,10 +299,11 @@ func authClientCredential(c *fiber.Ctx, auth *AuthCtx, _ *TokenReq, now time.Tim
|
||||
ClientID: &auth.Client.ID,
|
||||
AccessToken: uuid.NewString(),
|
||||
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
|
||||
Scopes: &scopes,
|
||||
}
|
||||
|
||||
// 保存会话
|
||||
err := SaveSession(session)
|
||||
err := SaveSession(q.Q, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -261,71 +315,95 @@ func authPassword(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m
|
||||
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
|
||||
ua := u.X(c.Get(fiber.HeaderUserAgent))
|
||||
|
||||
// 分池认证
|
||||
var err error
|
||||
var user *m.User
|
||||
err := q.Q.Transaction(func(tx *q.Query) (err error) {
|
||||
switch req.LoginType {
|
||||
case GrantPasswordPhone:
|
||||
user, err = authUserBySms(tx, req.Username, req.Password)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
user = &m.User{
|
||||
Phone: req.Username,
|
||||
Username: u.P(req.Username),
|
||||
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
|
||||
}
|
||||
var admin *m.Admin
|
||||
|
||||
// 账户状态
|
||||
if user.Status == m.UserStatusDisabled {
|
||||
slog.Debug("账户状态异常", "username", req.Username, "status", user.Status)
|
||||
return core.NewBizErr("账号无法登录")
|
||||
var scopes []string
|
||||
|
||||
pool := req.LoginPool
|
||||
if pool == "" {
|
||||
pool = PwdLoginAsUser
|
||||
}
|
||||
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{
|
||||
Phone: req.Username,
|
||||
Status: m.UserStatusEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户的登录时间
|
||||
user.LastLogin = u.P(time.Now())
|
||||
user.LastLoginIP = ip
|
||||
user.LastLoginUA = ua
|
||||
if err := tx.User.Save(user); err != nil {
|
||||
return err
|
||||
|
||||
case PwdLoginAsAdmin:
|
||||
admin, err = authAdmin(req.LoginType, req.Username, req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scopes, err = adminScopes(admin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 非锁定管理员,不允许为空权限
|
||||
if !admin.Lock && (len(scopes) == 0) {
|
||||
return nil, ErrAuthorizeInvalidScope // 没有配置权限
|
||||
}
|
||||
|
||||
// 更新管理员登录时间
|
||||
admin.LastLogin = u.P(time.Now())
|
||||
admin.LastLoginIP = ip
|
||||
admin.LastLoginUA = ua
|
||||
|
||||
default:
|
||||
return nil, ErrAuthorizeInvalidRequest
|
||||
}
|
||||
|
||||
// 生成会话
|
||||
session := &m.Session{
|
||||
IP: ip,
|
||||
UA: ua,
|
||||
UserID: &user.ID,
|
||||
ClientID: &auth.Client.ID,
|
||||
Scopes: u.X(req.Scope),
|
||||
Scopes: u.X(strings.Join(scopes, " ")),
|
||||
AccessToken: uuid.NewString(),
|
||||
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
|
||||
}
|
||||
|
||||
if req.Remember {
|
||||
session.RefreshToken = u.P(uuid.NewString())
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -353,7 +431,7 @@ func authRefreshToken(_ *fiber.Ctx, _ *AuthCtx, req *TokenReq, now time.Time) (*
|
||||
}
|
||||
|
||||
// 保存令牌
|
||||
err = SaveSession(session)
|
||||
err = SaveSession(q.Q, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -394,12 +472,111 @@ func sendError(c *fiber.Ctx, err error, description ...string) error {
|
||||
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
|
||||
}
|
||||
|
||||
func Introspect() error {
|
||||
return nil
|
||||
type RevokeReq struct {
|
||||
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 = u.MaskPhone(profile.Phone)
|
||||
}
|
||||
if profile.IDNo != nil && *profile.IDNo != "" {
|
||||
profile.IDNo = u.P(u.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.
|
||||
Preload(q.Admin.Roles, q.Admin.Roles.Permissions).
|
||||
Where(q.Admin.ID.Eq(authCtx.Admin.ID)).
|
||||
Omit(q.Admin.DeletedAt, q.Admin.Password).
|
||||
Take()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 整理权限列表
|
||||
scopes := make(map[string]struct{}, 0)
|
||||
for _, role := range profile.Roles {
|
||||
for _, permission := range role.Permissions {
|
||||
scopes[permission.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
list := make([]string, 0, len(scopes))
|
||||
for scope := range scopes {
|
||||
list = append(list, scope)
|
||||
}
|
||||
|
||||
return ctx.JSON(struct {
|
||||
*m.Admin
|
||||
Scopes []string `json:"scopes"`
|
||||
}{profile, list})
|
||||
}
|
||||
|
||||
type CodeContext struct {
|
||||
@@ -6,15 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"platform/web/core"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
s "platform/web/services"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func Authenticate() fiber.Handler {
|
||||
@@ -118,72 +113,14 @@ func authBasic(_ context.Context, token string) (*AuthCtx, error) {
|
||||
return nil, fmt.Errorf("客户端认证失败:%w", err)
|
||||
}
|
||||
|
||||
return &AuthCtx{
|
||||
Client: client,
|
||||
Scopes: []string{},
|
||||
}, 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("客户端密钥错误")
|
||||
scopes := []string{}
|
||||
if client.Permissions != nil {
|
||||
for _, p := range client.Permissions {
|
||||
scopes = append(scopes, p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
return &AuthCtx{
|
||||
Client: client,
|
||||
Scopes: scopes,
|
||||
}, nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ func FindSession(accessToken string, now time.Time) (*m.Session, error) {
|
||||
Preload(field.Associations).
|
||||
Where(
|
||||
q.Session.AccessToken.Eq(accessToken),
|
||||
q.Session.AccessTokenExpires.Gt(now),
|
||||
q.Session.AccessTokenExpires.Gt(now.UTC()),
|
||||
).First()
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error
|
||||
Preload(field.Associations).
|
||||
Where(
|
||||
q.Session.RefreshToken.Eq(refreshToken),
|
||||
q.Session.RefreshTokenExpires.Gt(now),
|
||||
q.Session.RefreshTokenExpires.Gt(now.UTC()),
|
||||
).First()
|
||||
}
|
||||
|
||||
func SaveSession(session *m.Session) error {
|
||||
return q.Session.Save(session)
|
||||
func SaveSession(tx *q.Query, session *m.Session) error {
|
||||
return tx.Session.Save(session)
|
||||
}
|
||||
|
||||
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {
|
||||
|
||||
@@ -19,7 +19,6 @@ type Err struct {
|
||||
func (e *Err) Error() string {
|
||||
if e.err != nil {
|
||||
slog.Debug(fmt.Sprintf("%s: %s", e.msg, e.err.Error()))
|
||||
return e.msg
|
||||
}
|
||||
return e.msg
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PageReq 分页请求参数
|
||||
type PageReq struct {
|
||||
RawPage int `json:"page"`
|
||||
@@ -38,3 +50,88 @@ type PageResp struct {
|
||||
Size int `json:"size"`
|
||||
List any `json:"list"`
|
||||
}
|
||||
|
||||
// Fetch 发送HTTP请求并返回响应
|
||||
func Fetch(req *http.Request) (*http.Response, error) {
|
||||
if env.DebugHttpDump {
|
||||
str, err := httputil.DumpRequest(req, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("===== REQUEST ===== %s\n", req.URL)
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if env.DebugHttpDump {
|
||||
str, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("===== RESPONSE ===== %s\n", req.URL)
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func Query(in any) url.Values {
|
||||
out := url.Values{}
|
||||
|
||||
if in == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
ref := reflect.ValueOf(in)
|
||||
if ref.Kind() == reflect.Pointer {
|
||||
ref = ref.Elem()
|
||||
}
|
||||
|
||||
if ref.Kind() != reflect.Struct {
|
||||
return out
|
||||
}
|
||||
|
||||
for i := 0; i < ref.NumField(); i++ {
|
||||
field := ref.Type().Field(i)
|
||||
value := ref.Field(i)
|
||||
|
||||
if field.Type.Kind() == reflect.Pointer {
|
||||
if value.IsNil() {
|
||||
continue
|
||||
}
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
name := field.Name
|
||||
tags := strings.Split(field.Tag.Get("query"), ",")
|
||||
if len(tags) > 0 && tags[0] != "" {
|
||||
name = tags[0]
|
||||
}
|
||||
|
||||
switch value := value.Interface().(type) {
|
||||
case string:
|
||||
out.Add(name, value)
|
||||
case int:
|
||||
out.Add(name, strconv.Itoa(value))
|
||||
case bool:
|
||||
if len(tags) > 1 && 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
|
||||
}
|
||||
|
||||
// 数据请求
|
||||
type IdReq struct {
|
||||
Id int32 `json:"id"`
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ type IModel interface {
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
ID int32 `json:"id" gorm:"column:id;primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"column:deleted_at"`
|
||||
ID int32 `json:"id,omitzero" gorm:"column:id;primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at,omitzero" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitzero" gorm:"column:updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (m *Model) GetID() int32 {
|
||||
|
||||
8
web/core/product.go
Normal file
8
web/core/product.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package core
|
||||
|
||||
type ProductCode string
|
||||
|
||||
var (
|
||||
ProductShort ProductCode = "short"
|
||||
ProductLong ProductCode = "long"
|
||||
)
|
||||
99
web/core/scopes.go
Normal file
99
web/core/scopes.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package core
|
||||
|
||||
const (
|
||||
ScopePermission = string("permission") // 权限
|
||||
ScopePermissionRead = string("permission:read") // 读取权限列表
|
||||
ScopePermissionWrite = string("permission:write") // 写入权限
|
||||
|
||||
ScopeAdminRole = string("admin_role") // 管理员角色
|
||||
ScopeAdminRoleRead = string("admin_role:read") // 读取管理员角色列表
|
||||
ScopeAdminRoleWrite = string("admin_role:write") // 写入管理员角色
|
||||
|
||||
ScopeAdmin = string("admin") // 管理员
|
||||
ScopeAdminRead = string("admin:read") // 读取管理员列表
|
||||
ScopeAdminWrite = string("admin:write") // 写入管理员
|
||||
|
||||
ScopeProduct = string("product") // 产品
|
||||
ScopeProductRead = string("product:read") // 读取产品列表
|
||||
ScopeProductWrite = string("product:write") // 写入产品
|
||||
|
||||
ScopeProductSku = string("product_sku") // 产品套餐
|
||||
ScopeProductSkuRead = string("product_sku:read") // 读取产品套餐列表
|
||||
ScopeProductSkuWrite = string("product_sku:write") // 写入产品套餐
|
||||
ScopeProductSkuWriteStatus = string("product_sku:write:status") // 更改产品套餐状态
|
||||
|
||||
ScopeDiscount = string("discount") // 折扣
|
||||
ScopeDiscountRead = string("discount:read") // 读取折扣列表
|
||||
ScopeDiscountWrite = string("discount:write") // 写入折扣
|
||||
|
||||
ScopeResource = string("resource") // 用户套餐
|
||||
ScopeResourceRead = string("resource:read") // 读取用户套餐列表
|
||||
ScopeResourceWrite = string("resource:write") // 写入用户套餐
|
||||
|
||||
ScopeResourceShort = string("resource:short") // 短效动态套餐
|
||||
ScopeResourceShortRead = string("resource:short:read") // 读取用户短效动态套餐列表
|
||||
ScopeResourceShortReadOfUser = string("resource:short:read:of_user") // 读取指定用户的短效动态套餐列表
|
||||
|
||||
ScopeResourceLong = string("resource:long") // 长效动态套餐
|
||||
ScopeResourceLongRead = string("resource:long:read") // 读取用户长效动态套餐列表
|
||||
ScopeResourceLongReadOfUser = string("resource:long:read:of_user") // 读取指定用户的长效动态套餐列表
|
||||
|
||||
ScopeUser = string("user") // 用户
|
||||
ScopeUserRead = string("user:read") // 读取用户列表
|
||||
ScopeUserReadOne = string("user:read:one") // 读取单个用户
|
||||
ScopeUserReadNotBind = string("user:read:not_bind") // 读取未绑定管理员的用户列表
|
||||
ScopeUserWrite = string("user:write") // 写入用户
|
||||
ScopeUserWriteBalance = string("user:write:balance") // 写入用户余额
|
||||
ScopeUserWriteBalanceInc = string("user:write:balance:inc") // 增加用户余额
|
||||
ScopeUserWriteBalanceDec = string("user:write:balance:dec") // 减少用户余额
|
||||
ScopeUserWriteBind = string("user:write:bind") // 用户认领
|
||||
|
||||
ScopeCoupon = string("coupon") // 优惠券
|
||||
ScopeCouponRead = string("coupon:read") // 读取优惠券列表
|
||||
ScopeCouponWrite = string("coupon:write") // 写入优惠券
|
||||
ScopeCouponWriteAssign = string("coupon:write:assign") // 发放优惠券
|
||||
|
||||
ScopeCouponUser = string("coupon_user") // 用户优惠券
|
||||
ScopeCouponUserRead = string("coupon_user:read") // 读取用户优惠券列表
|
||||
ScopeCouponUserReadOfUser = string("coupon_user:read:of_user") // 读取指定用户的用户优惠券列表
|
||||
ScopeCouponUserWrite = string("coupon_user:write") // 写入用户优惠券
|
||||
|
||||
ScopeBatch = string("batch") // 批次
|
||||
ScopeBatchRead = string("batch:read") // 读取批次列表
|
||||
ScopeBatchReadOfUser = string("batch:read:of_user") // 读取指定用户的批次列表
|
||||
ScopeBatchWrite = string("batch:write") // 写入批次
|
||||
|
||||
ScopeChannel = string("channel") // IP
|
||||
ScopeChannelRead = string("channel:read") // 读取 IP 列表
|
||||
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表
|
||||
ScopeChannelWrite = string("channel:write") // 写入 IP
|
||||
ScopeChannelWriteClearExpired = string("channel:write:clear_expired") // 清理过期 IP
|
||||
|
||||
ScopeProxy = string("proxy") // 代理
|
||||
ScopeProxyRead = string("proxy:read") // 读取代理列表
|
||||
ScopeProxyWrite = string("proxy:write") // 写入代理
|
||||
ScopeProxyWriteStatus = string("proxy:write:status") // 更改代理状态
|
||||
|
||||
ScopeArticle = string("article") // 文档
|
||||
ScopeArticleRead = string("article:read") // 读取文档列表
|
||||
ScopeArticleWrite = string("article:write") // 写入文档
|
||||
|
||||
ScopeArticleGroup = string("article_group") // 文档分组
|
||||
ScopeArticleGroupRead = string("article_group:read") // 读取文档分组列表
|
||||
ScopeArticleGroupWrite = string("article_group:write") // 写入文档分组
|
||||
|
||||
ScopeTrade = string("trade") // 交易
|
||||
ScopeTradeRead = string("trade:read") // 读取交易列表
|
||||
ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表
|
||||
ScopeTradeWrite = string("trade:write") // 写入交易
|
||||
ScopeTradeWriteComplete = string("trade:write:complete") // 完成交易
|
||||
|
||||
ScopeBill = string("bill") // 账单
|
||||
ScopeBillRead = string("bill:read") // 读取账单列表
|
||||
ScopeBillReadOfUser = string("bill:read:of_user") // 读取指定用户的账单列表
|
||||
ScopeBillWrite = string("bill:write") // 写入账单
|
||||
|
||||
ScopeBalanceActivity = string("balance_activity") // 余额变动
|
||||
ScopeBalanceActivityRead = string("balance_activity:read") // 读取余额变动列表
|
||||
ScopeBalanceActivityReadOfUser = string("balance_activity:read:of_user") // 读取指定用户的余额变动列表
|
||||
)
|
||||
31
web/error.go
31
web/error.go
@@ -1,11 +1,14 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
@@ -19,6 +22,9 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
|
||||
var authErr auth.AuthErr
|
||||
var bizErr *core.BizErr
|
||||
var servErr *core.ServErr
|
||||
var timeErr *time.ParseError
|
||||
var jsonErr *json.UnmarshalTypeError
|
||||
var jsonSyntaxErr *json.SyntaxError
|
||||
|
||||
switch {
|
||||
|
||||
@@ -46,11 +52,32 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
|
||||
|
||||
case errors.As(err, &servErr):
|
||||
code = fiber.StatusInternalServerError
|
||||
message = err.Error()
|
||||
slog.Warn("服务端错误", slog.String("error", servErr.Error()))
|
||||
message = "服务端错误"
|
||||
|
||||
case errors.As(err, &timeErr):
|
||||
code = fiber.StatusBadRequest
|
||||
message = fmt.Sprintf("时间格式不正确,传入值为 %s,检查传参是否为时间类型", timeErr.Value)
|
||||
|
||||
case errors.As(err, &jsonErr):
|
||||
code = fiber.StatusBadRequest
|
||||
message = fmt.Sprintf("参数 %s 类型不正确,传入类型为 %s,正确类型应该为 %s", jsonErr.Field, jsonErr.Value, jsonErr.Type.Name())
|
||||
|
||||
case errors.As(err, &jsonSyntaxErr):
|
||||
code = fiber.StatusBadRequest
|
||||
message = "参数格式不正确,检查传参是否为 JSON 格式"
|
||||
|
||||
// 所有未手动声明的错误类型
|
||||
default:
|
||||
slog.Warn("未处理的异常", slog.String("type", reflect.TypeOf(err).Name()), slog.String("error", err.Error()))
|
||||
t := reflect.TypeOf(err)
|
||||
for {
|
||||
if t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
slog.Warn("未处理的异常", slog.String("type", t.String()), slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
const RemoveChannel = "channel:remove"
|
||||
|
||||
type RemoveChannelData struct {
|
||||
Batch string `json:"batch"`
|
||||
IDs []int32 `json:"ids"`
|
||||
}
|
||||
|
||||
func NewRemoveChannel(data RemoveChannelData) *asynq.Task {
|
||||
bytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
slog.Error("序列化删除通道任务失败", "error", err)
|
||||
return nil
|
||||
}
|
||||
return asynq.NewTask(RemoveChannel, bytes)
|
||||
func NewRemoveChannel(batch string) *asynq.Task {
|
||||
return asynq.NewTask(RemoveChannel, []byte(batch))
|
||||
}
|
||||
|
||||
9
web/events/edges.go
Normal file
9
web/events/edges.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package events
|
||||
|
||||
import "github.com/hibiken/asynq"
|
||||
|
||||
const RefreshEdge = "edge:refresh"
|
||||
|
||||
func NewRefreshEdge() *asynq.Task {
|
||||
return asynq.NewTask(RefreshEdge, nil)
|
||||
}
|
||||
@@ -9,18 +9,23 @@ import (
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
const CompleteTrade = "trade:update"
|
||||
const CloseTrade = "trade:update"
|
||||
|
||||
type CompleteTradeData struct {
|
||||
type CloseTradeData struct {
|
||||
UserId int32 `json:"user_id" validate:"required"`
|
||||
TradeNo string `json:"trade_no" validate:"required"`
|
||||
Method m.TradeMethod `json:"method" validate:"required"`
|
||||
}
|
||||
|
||||
func NewCancelTrade(data CompleteTradeData) *asynq.Task {
|
||||
bytes, err := json.Marshal(data)
|
||||
func NewCloseTradeTask(uid int32, tradeNo string, method m.TradeMethod) *asynq.Task {
|
||||
bytes, err := json.Marshal(CloseTradeData{
|
||||
UserId: uid,
|
||||
TradeNo: tradeNo,
|
||||
Method: method,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("序列化更新交易任务失败", "error", err)
|
||||
return nil
|
||||
}
|
||||
return asynq.NewTask(CompleteTrade, bytes)
|
||||
return asynq.NewTask(CloseTrade, bytes)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"platform/pkg/env"
|
||||
"platform/web/core"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,18 +18,12 @@ import (
|
||||
|
||||
// CloudClient 定义云服务接口
|
||||
type CloudClient interface {
|
||||
CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error)
|
||||
CloudConnect(param CloudConnectReq) error
|
||||
CloudDisconnect(param CloudDisconnectReq) (int, error)
|
||||
CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error)
|
||||
CloudConnect(param *CloudConnectReq) error
|
||||
CloudDisconnect(param *CloudDisconnectReq) (int, error)
|
||||
CloudAutoQuery() (CloudConnectResp, error)
|
||||
}
|
||||
|
||||
// GatewayClient 定义网关接口
|
||||
type GatewayClient interface {
|
||||
GatewayPortConfigs(params []PortConfigsReq) error
|
||||
GatewayPortActive(param ...PortActiveReq) (map[string]PortData, error)
|
||||
}
|
||||
|
||||
type cloud struct {
|
||||
url string
|
||||
}
|
||||
@@ -37,59 +32,14 @@ var Cloud CloudClient
|
||||
|
||||
func initBaiyin() error {
|
||||
Cloud = &cloud{
|
||||
url: env.BaiyinAddr,
|
||||
url: env.BaiyinCloudUrl,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AutoConfig struct {
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Isp string `json:"isp"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// region cloud:/edges
|
||||
|
||||
type CloudEdgesReq struct {
|
||||
Province string
|
||||
City string
|
||||
Isp string
|
||||
Offset int
|
||||
Limit int
|
||||
}
|
||||
|
||||
type CloudEdgesResp struct {
|
||||
Edges []Edge `json:"edges"`
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
EdgesId int `json:"edges_id"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Isp string `json:"isp"`
|
||||
Ip string `json:"ip"`
|
||||
Rtt int `json:"rtt"`
|
||||
PacketLoss int `json:"packet_loss"`
|
||||
}
|
||||
|
||||
func (c *cloud) CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) {
|
||||
data := strings.Builder{}
|
||||
data.WriteString("province=")
|
||||
data.WriteString(param.Province)
|
||||
data.WriteString("&city=")
|
||||
data.WriteString(param.City)
|
||||
data.WriteString("&isp=")
|
||||
data.WriteString(param.Isp)
|
||||
data.WriteString("&offset=")
|
||||
data.WriteString(strconv.Itoa(param.Offset))
|
||||
data.WriteString("&limit=")
|
||||
data.WriteString(strconv.Itoa(param.Limit))
|
||||
|
||||
resp, err := c.requestCloud("GET", "/edges?"+data.String(), "")
|
||||
// cloud:/edges 筛选查询边缘节点
|
||||
func (c *cloud) CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error) {
|
||||
resp, err := c.requestCloud("GET", "/edges?"+core.Query(param).Encode(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -115,17 +65,46 @@ func (c *cloud) CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region cloud:/connect
|
||||
|
||||
type CloudConnectReq struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Edge []string `json:"edge,omitempty"`
|
||||
AutoConfig []AutoConfig `json:"auto_config,omitempty"`
|
||||
type CloudEdgesReq struct {
|
||||
Province *string `query:"province"`
|
||||
City *string `query:"city"`
|
||||
Isp *string `query:"isp"`
|
||||
Offset *int `query:"offset"`
|
||||
Limit *int `query:"limit"`
|
||||
NoRepeat *bool `query:"norepeat,b2i"`
|
||||
NoDayRepeat *bool `query:"nodayrepeat,b2i"`
|
||||
IpUnchangedTime *int `query:"ip_unchanged_time"` // 单位秒
|
||||
ActiveTime *int `query:"active_time"` // 单位秒
|
||||
// 排序方式,可选值:
|
||||
// - create_time_asc 设备创建时间顺序
|
||||
// - create_time_desc 设备创建时间倒序
|
||||
// - ip_unchanged_time_asc ip持续没变化时间顺序
|
||||
// - ip_unchanged_time_desc ip持续没变化时间倒序
|
||||
// - active_time_asc 连续活跃时间顺序
|
||||
// - active_time_desc 连续活跃时间倒序
|
||||
// - rand 随机排序 (默认)
|
||||
Sort *string `query:"sort"`
|
||||
}
|
||||
|
||||
func (c *cloud) CloudConnect(param CloudConnectReq) error {
|
||||
type CloudEdgesResp struct {
|
||||
Edges []Edge `json:"edges"`
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
EdgeID string `json:"edge_id"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Isp string `json:"isp"`
|
||||
Ip string `json:"ip"`
|
||||
Rtt int `json:"rtt"`
|
||||
PacketLoss int `json:"packet_loss"`
|
||||
}
|
||||
|
||||
// cloud:/connect 连接边缘节点到网关
|
||||
func (c *cloud) CloudConnect(param *CloudConnectReq) error {
|
||||
data, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -162,25 +141,21 @@ func (c *cloud) CloudConnect(param CloudConnectReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region cloud:/disconnect
|
||||
|
||||
type CloudDisconnectReq struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Edge []string `json:"edge,omitempty"`
|
||||
Config []Config `json:"auto_config,omitempty"`
|
||||
type CloudConnectReq struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Edge *[]string `json:"edge,omitempty"`
|
||||
AutoConfig *[]AutoConfig `json:"auto_config,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
type AutoConfig struct {
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Isp string `json:"isp"`
|
||||
Count int `json:"count"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
|
||||
func (c *cloud) CloudDisconnect(param CloudDisconnectReq) (int, error) {
|
||||
// cloud:/disconnect 解除连接边缘节点到网关
|
||||
func (c *cloud) CloudDisconnect(param *CloudDisconnectReq) (int, error) {
|
||||
data, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -217,12 +192,21 @@ func (c *cloud) CloudDisconnect(param CloudDisconnectReq) (int, error) {
|
||||
return int(result["disconnected_edges"].(float64)), nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
type CloudDisconnectReq struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Edge *[]string `json:"edge,omitempty"`
|
||||
Config *[]Config `json:"auto_config,omitempty"`
|
||||
}
|
||||
|
||||
// region cloud:/auto_query
|
||||
|
||||
type CloudConnectResp map[string][]AutoConfig
|
||||
type Config struct {
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Isp string `json:"isp"`
|
||||
Count int `json:"count"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
|
||||
// cloud:/auto_query 自动连接配置查询
|
||||
func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
|
||||
resp, err := c.requestCloud("GET", "/auto_query", "")
|
||||
if err != nil {
|
||||
@@ -250,7 +234,7 @@ func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
type CloudConnectResp map[string][]AutoConfig
|
||||
|
||||
func (c *cloud) requestCloud(method string, url string, data string) (*http.Response, error) {
|
||||
|
||||
@@ -263,7 +247,7 @@ func (c *cloud) requestCloud(method string, url string, data string) (*http.Resp
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
var resp *http.Response
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
token, err := c.token(i == 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -304,7 +288,7 @@ func (c *cloud) requestCloud(method string, url string, data string) (*http.Resp
|
||||
func (c *cloud) token(refresh bool) (string, error) {
|
||||
// redis 获取令牌
|
||||
if !refresh {
|
||||
token, err := Redis.Get(context.Background(), "remote:token").Result()
|
||||
token, err := Redis.Get(context.Background(), BaiyinToken).Result()
|
||||
if err == nil && token != "" {
|
||||
return token, nil
|
||||
}
|
||||
@@ -338,7 +322,7 @@ func (c *cloud) token(refresh bool) (string, error) {
|
||||
var result map[string]any
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("解析响应 [%s] 失败: %w", string(body), err)
|
||||
}
|
||||
|
||||
if result["code"].(float64) != 1 {
|
||||
@@ -347,7 +331,7 @@ func (c *cloud) token(refresh bool) (string, error) {
|
||||
|
||||
// redis 设置令牌
|
||||
token := result["token"].(string)
|
||||
err = Redis.Set(context.Background(), "remote:token", token, 1*time.Hour).Err()
|
||||
err = Redis.Set(context.Background(), BaiyinToken, token, 1*time.Hour).Err()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -355,6 +339,15 @@ func (c *cloud) token(refresh bool) (string, error) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
const BaiyinToken = "clients:baiyin:token"
|
||||
|
||||
// GatewayClient 定义网关接口
|
||||
type GatewayClient interface {
|
||||
GatewayPortConfigs(params []*PortConfigsReq) error
|
||||
GatewayPortActive(param ...*PortActiveReq) (map[string]PortData, error)
|
||||
GatewayEdge(params *GatewayEdgeReq) (map[string]GatewayEdgeInfo, error)
|
||||
}
|
||||
|
||||
type gateway struct {
|
||||
url string
|
||||
username string
|
||||
@@ -373,6 +366,68 @@ func NewGateway(url, username, password string) GatewayClient {
|
||||
return GatewayInitializer(url, username, password)
|
||||
}
|
||||
|
||||
type GatewayEdgeReq struct {
|
||||
EdgeID *string `query:"edge_id"`
|
||||
Province *string `query:"province"`
|
||||
City *string `query:"city"`
|
||||
Isp *string `query:"isp"`
|
||||
Connected *bool `query:"connected"`
|
||||
Assigned *bool `query:"assigned"`
|
||||
GetRand *int `query:"getRand"`
|
||||
IpUnchangedTimeStart *int `query:"ip_unchanged_time_start"`
|
||||
IpUnchangedTimeEnd *int `query:"ip_unchanged_time_end"`
|
||||
OnlineTimeStart *int `query:"online_time_start"`
|
||||
OnlineTimeEnd *int `query:"online_time_end"`
|
||||
Rtt *int `query:"rtt"`
|
||||
MinRtt *int `query:"min_rtt"`
|
||||
RttBaidu *int `query:"rtt_baidu"`
|
||||
PacketLoss *int `query:"packet_loss"`
|
||||
PacketLossBaidu *int `query:"packet_loss_baidu"`
|
||||
IP *string `query:"ip"`
|
||||
Limit *int `query:"limit"`
|
||||
Offset *int `query:"offset"`
|
||||
}
|
||||
|
||||
type GatewayEdgeResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data map[string]GatewayEdgeInfo `json:"data"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type GatewayEdgeInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Connected bool `json:"connected"`
|
||||
Assigned bool `json:"assigned"`
|
||||
AssignedTo string `json:"assignedto"`
|
||||
PacketLoss int `json:"packet_loss"`
|
||||
PacketLossBaidu int `json:"packet_loss_baidu"`
|
||||
Rtt int `json:"rtt"`
|
||||
RttBaidu int `json:"rtt_baidu"`
|
||||
OfflineTime int `json:"offline_time"`
|
||||
OnlineTime int `json:"online_time"`
|
||||
IpUnchangedTime int `json:"ip_unchanged_time"`
|
||||
}
|
||||
|
||||
func (c *gateway) GatewayEdge(req *GatewayEdgeReq) (map[string]GatewayEdgeInfo, error) {
|
||||
resp, err := c.get("/edge", core.Query(req))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询可用节点失败:%w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body := new(GatewayEdgeResp)
|
||||
if err = json.NewDecoder(resp.Body).Decode(body); err != nil {
|
||||
return nil, fmt.Errorf("解析响应内容失败:%w", err)
|
||||
}
|
||||
|
||||
if body.Code != 0 {
|
||||
return nil, fmt.Errorf("接口业务响应异常: %d %s", body.Code, body.Msg)
|
||||
}
|
||||
|
||||
return body.Data, nil
|
||||
}
|
||||
|
||||
// region gateway:/port/configs
|
||||
|
||||
type PortConfigsReq struct {
|
||||
@@ -395,7 +450,7 @@ type AutoEdgeConfig struct {
|
||||
PacketLoss int `json:"packet_loss,omitempty"`
|
||||
}
|
||||
|
||||
func (c *gateway) GatewayPortConfigs(params []PortConfigsReq) error {
|
||||
func (c *gateway) GatewayPortConfigs(params []*PortConfigsReq) error {
|
||||
if len(params) == 0 {
|
||||
return errors.New("params is empty")
|
||||
}
|
||||
@@ -461,10 +516,10 @@ type PortData struct {
|
||||
Userpass string `json:"userpass"`
|
||||
}
|
||||
|
||||
func (c *gateway) GatewayPortActive(param ...PortActiveReq) (map[string]PortData, error) {
|
||||
func (c *gateway) GatewayPortActive(param ...*PortActiveReq) (map[string]PortData, error) {
|
||||
_param := PortActiveReq{}
|
||||
if len(param) != 0 {
|
||||
_param = param[0]
|
||||
if len(param) != 0 && param[0] != nil {
|
||||
_param = *param[0]
|
||||
}
|
||||
|
||||
path := strings.Builder{}
|
||||
@@ -520,38 +575,33 @@ func (c *gateway) GatewayPortActive(param ...PortActiveReq) (map[string]PortData
|
||||
|
||||
// endregion
|
||||
|
||||
func (c *gateway) get(url string, params url.Values) (*http.Response, error) {
|
||||
url = fmt.Sprintf("http://%s:%s@%s:9990%s?%s", c.username, c.password, c.url, url, params.Encode())
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败:%w", err)
|
||||
}
|
||||
|
||||
res, err := core.Fetch(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取数据失败:%w", err)
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
bytes, _ := io.ReadAll(res.Body)
|
||||
return nil, fmt.Errorf("接口响应异常: %d %s", res.StatusCode, string(bytes))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *gateway) requestGateway(method string, url string, data string) (*http.Response, error) {
|
||||
//goland:noinspection ALL
|
||||
url = fmt.Sprintf("http://%s:%s@%s:9990%s", c.username, c.password, c.url, url)
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if env.DebugHttpDump {
|
||||
str, err := httputil.DumpRequest(req, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("==============================")
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if env.DebugHttpDump {
|
||||
str, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("------------------------------")
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return core.Fetch(req)
|
||||
}
|
||||
|
||||
280
web/globals/gost.go
Normal file
280
web/globals/gost.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package globals
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"platform/web/core"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrGostNotFound = errors.New("gost resource not found")
|
||||
|
||||
func IsGostNotFound(err error) bool {
|
||||
return errors.Is(err, ErrGostNotFound)
|
||||
}
|
||||
|
||||
type GostClient interface {
|
||||
ListChains() ([]*GostChainConfig, error)
|
||||
GetChain(name string) (*GostChainConfig, error)
|
||||
CreateChain(chain *GostChainConfig) error
|
||||
DeleteChain(name string) error
|
||||
SaveConfig() error
|
||||
CreateService(service *GostServiceConfig) error
|
||||
DeleteService(name string) error
|
||||
CreateAuther(auther *GostAutherConfig) error
|
||||
DeleteAuther(name string) error
|
||||
CreateAdmission(admission *GostAdmissionConfig) error
|
||||
DeleteAdmission(name string) error
|
||||
}
|
||||
|
||||
type gostClient struct {
|
||||
baseURL string
|
||||
pathPrefix string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
var GostInitializer = func(host string, port int, pathPrefix, username, password string) GostClient {
|
||||
baseURL := strings.TrimSpace(host)
|
||||
if !strings.Contains(baseURL, "://") {
|
||||
baseURL = fmt.Sprintf("http://%s:%d", baseURL, port)
|
||||
}
|
||||
|
||||
return &gostClient{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
pathPrefix: normalizeGostPathPrefix(pathPrefix),
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func NewGost(host string, port int, pathPrefix, username, password string) GostClient {
|
||||
return GostInitializer(host, port, pathPrefix, username, password)
|
||||
}
|
||||
|
||||
type GostChainConfig struct {
|
||||
Name string `json:"name"`
|
||||
Hops []GostHopConfig `json:"hops,omitempty"`
|
||||
}
|
||||
|
||||
type GostHopConfig struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Nodes []GostNodeConfig `json:"nodes,omitempty"`
|
||||
}
|
||||
|
||||
type GostNodeConfig struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Addr string `json:"addr"`
|
||||
Connector GostConnectorConfig `json:"connector"`
|
||||
Dialer GostDialerConfig `json:"dialer"`
|
||||
}
|
||||
|
||||
type GostConnectorConfig struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GostDialerConfig struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GostServiceConfig struct {
|
||||
Name string `json:"name"`
|
||||
Addr string `json:"addr"`
|
||||
Admission string `json:"admission,omitempty"`
|
||||
Handler GostHandlerConfig `json:"handler"`
|
||||
Listener GostListenerConfig `json:"listener"`
|
||||
Recorders []GostRecorderConfig `json:"recorders,omitempty"`
|
||||
}
|
||||
|
||||
type GostHandlerConfig struct {
|
||||
Type string `json:"type"`
|
||||
Chain string `json:"chain,omitempty"`
|
||||
Auther string `json:"auther,omitempty"`
|
||||
}
|
||||
|
||||
type GostListenerConfig struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GostRecorderConfig struct {
|
||||
Name string `json:"name"`
|
||||
Record string `json:"record"`
|
||||
}
|
||||
|
||||
type GostAutherConfig struct {
|
||||
Name string `json:"name"`
|
||||
Auths []GostAuthConfig `json:"auths"`
|
||||
}
|
||||
|
||||
type GostAuthConfig struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type GostAdmissionConfig struct {
|
||||
Name string `json:"name"`
|
||||
Whitelist bool `json:"whitelist"`
|
||||
Matchers []string `json:"matchers"`
|
||||
}
|
||||
|
||||
func (c *gostClient) GetChain(name string) (*GostChainConfig, error) {
|
||||
body, err := c.get("/config/chains/" + url.PathEscape(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return &GostChainConfig{Name: name}, nil
|
||||
}
|
||||
|
||||
var direct GostChainConfig
|
||||
if err := json.Unmarshal(body, &direct); err == nil && direct.Name != "" {
|
||||
return &direct, nil
|
||||
}
|
||||
|
||||
var wrapper struct {
|
||||
Data *GostChainConfig `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapper); err == nil && wrapper.Data != nil && wrapper.Data.Name != "" {
|
||||
return wrapper.Data, nil
|
||||
}
|
||||
|
||||
return &GostChainConfig{Name: name}, nil
|
||||
}
|
||||
|
||||
func (c *gostClient) ListChains() ([]*GostChainConfig, error) {
|
||||
body, err := c.get("/config/chains")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Data struct {
|
||||
Count int `json:"count"`
|
||||
List []*GostChainConfig `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil, fmt.Errorf("parse gost chain list failed %s: %w", string(body), err)
|
||||
}
|
||||
|
||||
return resp.Data.List, nil
|
||||
}
|
||||
|
||||
func (c *gostClient) CreateChain(chain *GostChainConfig) error {
|
||||
return c.create("/config/chains", chain)
|
||||
}
|
||||
|
||||
func (c *gostClient) DeleteChain(name string) error {
|
||||
return c.delete("/config/chains/" + url.PathEscape(name))
|
||||
}
|
||||
|
||||
func (c *gostClient) SaveConfig() error {
|
||||
return c.create("/config", nil)
|
||||
}
|
||||
|
||||
func (c *gostClient) CreateService(service *GostServiceConfig) error {
|
||||
return c.create("/config/services", service)
|
||||
}
|
||||
|
||||
func (c *gostClient) DeleteService(name string) error {
|
||||
return c.delete("/config/services/" + url.PathEscape(name))
|
||||
}
|
||||
|
||||
func (c *gostClient) CreateAuther(auther *GostAutherConfig) error {
|
||||
return c.create("/config/authers", auther)
|
||||
}
|
||||
|
||||
func (c *gostClient) DeleteAuther(name string) error {
|
||||
return c.delete("/config/authers/" + url.PathEscape(name))
|
||||
}
|
||||
|
||||
func (c *gostClient) CreateAdmission(admission *GostAdmissionConfig) error {
|
||||
return c.create("/config/admissions", admission)
|
||||
}
|
||||
|
||||
func (c *gostClient) DeleteAdmission(name string) error {
|
||||
return c.delete("/config/admissions/" + url.PathEscape(name))
|
||||
}
|
||||
|
||||
func (c *gostClient) create(path string, payload any) error {
|
||||
_, err := c.request(http.MethodPost, path, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *gostClient) get(path string) ([]byte, error) {
|
||||
body, err := c.request(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (c *gostClient) delete(path string) error {
|
||||
_, err := c.request(http.MethodDelete, path, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *gostClient) request(method string, path string, payload any) ([]byte, error) {
|
||||
var bodyReader io.Reader
|
||||
if payload != nil {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.endpoint(path), bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := core.Fetch(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(resp.Body)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
return nil, fmt.Errorf("%w: %s", ErrGostNotFound, string(body))
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("gost api %s %s failed: %d %s", method, path, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (c *gostClient) endpoint(path string) string {
|
||||
return c.baseURL + c.pathPrefix + path
|
||||
}
|
||||
|
||||
func normalizeGostPathPrefix(prefix string) string {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(prefix, "/") {
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
return strings.TrimRight(prefix, "/")
|
||||
}
|
||||
107
web/globals/gost_test.go
Normal file
107
web/globals/gost_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package globals
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGostClientChainOperations(t *testing.T) {
|
||||
var (
|
||||
created *GostChainConfig
|
||||
deleted []string
|
||||
saved bool
|
||||
)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok || username != "user" || password != "pass" {
|
||||
t.Errorf("unexpected auth: ok=%v username=%q password=%q", ok, username, password)
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/config/chains":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"count": 2,
|
||||
"list": []map[string]any{
|
||||
{"name": "old-a"},
|
||||
{"name": "old-b"},
|
||||
},
|
||||
})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/config/chains":
|
||||
if err := json.NewDecoder(r.Body).Decode(&created); err != nil {
|
||||
t.Errorf("Decode chain failed: %v", err)
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-a":
|
||||
deleted = append(deleted, "old-a")
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
case r.Method == http.MethodDelete && r.URL.Path == "/api/config/chains/old-b":
|
||||
deleted = append(deleted, "old-b")
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/config":
|
||||
saved = true
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
default:
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewGost(server.URL, 9700, "/api", "user", "pass")
|
||||
|
||||
chains, err := client.ListChains()
|
||||
if err != nil {
|
||||
t.Fatalf("ListChains returned error: %v", err)
|
||||
}
|
||||
if len(chains) != 2 || chains[0].Name != "old-a" || chains[1].Name != "old-b" {
|
||||
t.Fatalf("unexpected chains: %#v", chains)
|
||||
}
|
||||
|
||||
if err := client.DeleteChain(chains[0].Name); err != nil {
|
||||
t.Fatalf("DeleteChain old-a returned error: %v", err)
|
||||
}
|
||||
if err := client.DeleteChain(chains[1].Name); err != nil {
|
||||
t.Fatalf("DeleteChain old-b returned error: %v", err)
|
||||
}
|
||||
if len(deleted) != 2 {
|
||||
t.Fatalf("unexpected deleted chains: %#v", deleted)
|
||||
}
|
||||
|
||||
err = client.CreateChain(&GostChainConfig{
|
||||
Name: "edge-a",
|
||||
Hops: []GostHopConfig{{
|
||||
Nodes: []GostNodeConfig{{
|
||||
Addr: "192.0.2.1:1080",
|
||||
Connector: GostConnectorConfig{Type: "socks5"},
|
||||
Dialer: GostDialerConfig{Type: "tcp"},
|
||||
}},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChain returned error: %v", err)
|
||||
}
|
||||
if created == nil || created.Name != "edge-a" {
|
||||
t.Fatalf("unexpected created chain: %#v", created)
|
||||
}
|
||||
if len(created.Hops) != 1 || len(created.Hops[0].Nodes) != 1 {
|
||||
t.Fatalf("unexpected created chain hops: %#v", created.Hops)
|
||||
}
|
||||
node := created.Hops[0].Nodes[0]
|
||||
if node.Addr != "192.0.2.1:1080" || node.Connector.Type != "socks5" || node.Dialer.Type != "tcp" {
|
||||
t.Fatalf("unexpected created node: %#v", node)
|
||||
}
|
||||
|
||||
if err := client.SaveConfig(); err != nil {
|
||||
t.Fatalf("SaveConfig returned error: %v", err)
|
||||
}
|
||||
if !saved {
|
||||
t.Fatal("expected SaveConfig request")
|
||||
}
|
||||
}
|
||||
30
web/globals/orm/timez.go
Normal file
30
web/globals/orm/timez.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DateTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (dt *DateTime) Scan(value any) error {
|
||||
switch v := value.(type) {
|
||||
case time.Time:
|
||||
dt.Time = v
|
||||
case string:
|
||||
t, err := time.Parse(time.RFC3339, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dt.Time = t
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dt DateTime) Value() (any, error) {
|
||||
return dt.Time.Format(time.RFC3339), nil
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package globals
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"platform/pkg/env"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
@@ -16,8 +18,19 @@ import (
|
||||
var tp *trace.TracerProvider
|
||||
|
||||
func initOtel(ctx context.Context) error {
|
||||
addr := env.OtelHost + ":" + env.OtelPort
|
||||
name := "lanhu-platform"
|
||||
if env.OtelNameSuffix != "" {
|
||||
name += "-" + env.OtelNameSuffix
|
||||
}
|
||||
slog.Info("初始化 otel", "endpoint", addr, "service_suffix", name)
|
||||
|
||||
if env.OtelHost == "" || env.OtelPort == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
exporter, err := otlptracegrpc.New(ctx,
|
||||
otlptracegrpc.WithEndpoint("localhost:4317"),
|
||||
otlptracegrpc.WithEndpoint(addr),
|
||||
otlptracegrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -30,7 +43,7 @@ func initOtel(ctx context.Context) error {
|
||||
trace.WithResource(
|
||||
resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceNameKey.String("lanhu-platform"),
|
||||
semconv.ServiceNameKey.String(name),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/partnerpayments/h5"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
@@ -18,6 +19,7 @@ var WechatPay *WechatPayClient
|
||||
|
||||
type WechatPayClient struct {
|
||||
Native *native.NativeApiService
|
||||
H5 *h5.H5ApiService
|
||||
Notify *notify.Handler
|
||||
}
|
||||
|
||||
@@ -71,6 +73,7 @@ func initWechatPay() error {
|
||||
// 创建 WechatPay 服务
|
||||
WechatPay = &WechatPayClient{
|
||||
Native: &native.NativeApiService{Client: client},
|
||||
H5: &h5.H5ApiService{Client: client},
|
||||
Notify: handler,
|
||||
}
|
||||
return nil
|
||||
|
||||
102
web/handlers/admin.go
Normal file
102
web/handlers/admin.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func PageAdminByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.PageReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, total, err := s.Admin.Page(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
func AllAdminByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.Admin.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func CreateAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateAdmin
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Admin.Create(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
func UpdateAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateAdmin
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Admin.Update(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
func RemoveAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Admin.Remove(req.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
103
web/handlers/admin_role.go
Normal file
103
web/handlers/admin_role.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func AllAdminRoleByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRoleRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.AdminRole.ListRoles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func PageAdminRoleByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRoleRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageAdminRolesReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, total, err := s.AdminRole.PageRoles(req.PageReq)
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageAdminRolesReq struct {
|
||||
core.PageReq
|
||||
}
|
||||
|
||||
func CreateAdminRole(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRoleWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateAdminRole
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.AdminRole.CreateAdminRole(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
func UpdateAdminRole(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRoleWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateAdminRole
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.AdminRole.UpdateAdminRole(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
func RemoveAdminRole(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRoleWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.AdminRole.RemoveAdminRole(req.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
29
web/handlers/area.go
Normal file
29
web/handlers/area.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ListArea(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.Area.ListAreas()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
type ListAreaRespItem struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Level int `json:"level"`
|
||||
ParentID *int32 `json:"parent_id,omitempty"`
|
||||
}
|
||||
170
web/handlers/article.go
Normal file
170
web/handlers/article.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/pkg/env"
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func NavArticle(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.Article.Nav()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func GetArticle(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
article, err := s.Article.GetPublic(req.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(article)
|
||||
}
|
||||
|
||||
func PageArticleByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.PageArticleReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, total, err := s.Article.Page(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: list,
|
||||
})
|
||||
}
|
||||
|
||||
func GetArticleByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
article, err := s.Article.GetByAdmin(req.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(article)
|
||||
}
|
||||
|
||||
func CreateArticle(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateArticleData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Article.Create(req)
|
||||
}
|
||||
|
||||
func UpdateArticle(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateArticleData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Article.Update(req)
|
||||
}
|
||||
|
||||
func DeleteArticle(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Article.Delete(req.Id)
|
||||
}
|
||||
|
||||
func UploadArticleImage(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "缺少上传文件 file")
|
||||
}
|
||||
|
||||
result, err := s.Article.UploadImage(fileHeader, articleUploadBaseURL(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
func articleUploadBaseURL(c *fiber.Ctx) string {
|
||||
if env.UploadPublicBaseURL != "" {
|
||||
return strings.TrimRight(env.UploadPublicBaseURL, "/")
|
||||
}
|
||||
|
||||
scheme := c.Protocol()
|
||||
if forwardedProto := c.Get("X-Forwarded-Proto"); forwardedProto != "" {
|
||||
scheme = strings.TrimSpace(strings.Split(forwardedProto, ",")[0])
|
||||
}
|
||||
|
||||
host := c.Get(fiber.HeaderHost)
|
||||
if forwardedHost := c.Get("X-Forwarded-Host"); forwardedHost != "" {
|
||||
host = strings.TrimSpace(strings.Split(forwardedHost, ",")[0])
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
return scheme + "://" + host
|
||||
}
|
||||
90
web/handlers/article_group.go
Normal file
90
web/handlers/article_group.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func PageArticleGroupByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.PageArticleGroupReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, total, err := s.ArticleGroup.Page(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: list,
|
||||
})
|
||||
}
|
||||
|
||||
func ListArticleGroupByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.ArticleGroup.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func CreateArticleGroup(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateArticleGroupData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.ArticleGroup.Create(req)
|
||||
}
|
||||
|
||||
func UpdateArticleGroup(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateArticleGroupData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.ArticleGroup.Update(req)
|
||||
}
|
||||
|
||||
func DeleteArticleGroup(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.ArticleGroup.Delete(req.Id)
|
||||
}
|
||||
@@ -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
|
||||
189
web/handlers/balance_activity.go
Normal file
189
web/handlers/balance_activity.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
q "platform/web/queries"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// PageBalanceActivity 分页查询当前用户的余额变动记录
|
||||
func PageBalanceActivity(c *fiber.Ctx) error {
|
||||
// 获取当前用户ID
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageBalanceActivityByUserReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造查询条件
|
||||
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(authCtx.User.ID))
|
||||
if req.BillNo != nil {
|
||||
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
// 查询余额变动列表
|
||||
list, total, err := q.BalanceActivity.
|
||||
Joins(q.BalanceActivity.Bill).
|
||||
Select(
|
||||
q.BalanceActivity.ALL,
|
||||
q.Bill.As("Bill").ID.As("Bill__id"),
|
||||
q.Bill.As("Bill").BillNo.As("Bill__bill_no"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.BalanceActivity.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBalanceActivityByUserReq struct {
|
||||
core.PageReq
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
|
||||
// PageBalanceActivityByAdmin 分页查询所有余额变动记录
|
||||
func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeBalanceActivityRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageBalanceActivityByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造查询条件
|
||||
do := q.BalanceActivity.Where()
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.BillNo != nil {
|
||||
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
// 查询余额变动列表
|
||||
list, total, err := q.BalanceActivity.
|
||||
Joins(q.BalanceActivity.User, q.BalanceActivity.Admin, q.BalanceActivity.Bill).
|
||||
Select(
|
||||
q.BalanceActivity.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.Admin.As("Admin").Name.As("Admin__name"),
|
||||
q.Bill.As("Bill").BillNo.As("Bill__bill_no"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.BalanceActivity.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBalanceActivityByAdminReq struct {
|
||||
core.PageReq
|
||||
UserPhone *string `json:"user_phone,omitempty"`
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
|
||||
// PageBalanceActivityOfUserByAdmin 分页查询指定用户的余额变动记录
|
||||
func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeBalanceActivityReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageBalanceActivityOfUserByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造查询条件
|
||||
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(req.UserID))
|
||||
if req.BillNo != nil {
|
||||
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.BalanceActivity.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
// 查询余额变动列表
|
||||
list, total, err := q.BalanceActivity.
|
||||
Joins(q.BalanceActivity.Admin, q.BalanceActivity.Bill).
|
||||
Select(
|
||||
q.BalanceActivity.ALL,
|
||||
q.Admin.As("Admin").Name.As("Admin__name"),
|
||||
q.Bill.As("Bill").BillNo.As("Bill__bill_no"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.BalanceActivity.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBalanceActivityOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
205
web/handlers/batch.go
Normal file
205
web/handlers/batch.go
Normal file
@@ -0,0 +1,205 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// PageBatch 分页查询套餐提取记录
|
||||
func PageBatch(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.UTC()))
|
||||
}
|
||||
if req.TimeEnd != nil {
|
||||
conds.Where(q.LogsUserUsage.Time.Lte(req.TimeEnd.UTC()))
|
||||
}
|
||||
if req.ResourceNo != nil {
|
||||
conds.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
|
||||
list, total, err := q.LogsUserUsage.
|
||||
Joins(q.LogsUserUsage.Resource).
|
||||
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
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
TimeStart *time.Time `json:"time_start"`
|
||||
TimeEnd *time.Time `json:"time_end"`
|
||||
}
|
||||
|
||||
// PageBatchByAdmin 分页查询所有提取记录
|
||||
func PageBatchByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeBatchRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageBatchByAdminReq
|
||||
if err = g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do := q.LogsUserUsage.Where()
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.BatchNo != nil {
|
||||
do = do.Where(q.LogsUserUsage.BatchNo.Eq(*req.BatchNo))
|
||||
}
|
||||
if req.Prov != nil {
|
||||
do = do.Where(q.LogsUserUsage.Prov.Eq(*req.Prov))
|
||||
}
|
||||
if req.City != nil {
|
||||
do = do.Where(q.LogsUserUsage.City.Eq(*req.City))
|
||||
}
|
||||
if req.Isp != nil {
|
||||
do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.LogsUserUsage.Time.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.LogsUserUsage.Time.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
list, total, err := q.LogsUserUsage.
|
||||
Joins(q.LogsUserUsage.User, q.LogsUserUsage.Resource).
|
||||
Select(
|
||||
q.LogsUserUsage.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
|
||||
q.Resource.As("Resource").Type.As("Resource__type"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.LogsUserUsage.Time.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBatchByAdminReq struct {
|
||||
c.PageReq
|
||||
UserPhone *string `json:"user_phone"`
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
BatchNo *string `json:"batch_no"`
|
||||
Prov *string `json:"prov"`
|
||||
City *string `json:"city"`
|
||||
Isp *string `json:"isp"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end"`
|
||||
}
|
||||
|
||||
// PageBatchOfUserByAdmin 分页查询指定用户的提取记录
|
||||
func PageBatchOfUserByAdmin(ctx *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(ctx).PermitAdmin(core.ScopeBatchReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageBatchOfUserByAdminReq
|
||||
if err = g.Validator.ParseBody(ctx, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(req.UserID))
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.BatchNo != nil {
|
||||
do = do.Where(q.LogsUserUsage.BatchNo.Eq(*req.BatchNo))
|
||||
}
|
||||
if req.Prov != nil {
|
||||
do = do.Where(q.LogsUserUsage.Prov.Eq(*req.Prov))
|
||||
}
|
||||
if req.City != nil {
|
||||
do = do.Where(q.LogsUserUsage.City.Eq(*req.City))
|
||||
}
|
||||
if req.Isp != nil {
|
||||
do = do.Where(q.LogsUserUsage.ISP.Eq(*req.Isp))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.LogsUserUsage.Time.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.LogsUserUsage.Time.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
list, total, err := q.LogsUserUsage.
|
||||
Joins(q.LogsUserUsage.User, q.LogsUserUsage.Resource).
|
||||
Select(
|
||||
q.LogsUserUsage.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
|
||||
q.Resource.As("Resource").Type.As("Resource__type"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.LogsUserUsage.Time.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return ctx.JSON(core.NewBizErr("获取数据失败", err))
|
||||
}
|
||||
|
||||
return ctx.JSON(c.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBatchOfUserByAdminReq struct {
|
||||
c.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
BatchNo *string `json:"batch_no"`
|
||||
Prov *string `json:"prov"`
|
||||
City *string `json:"city"`
|
||||
Isp *string `json:"isp"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end"`
|
||||
}
|
||||
@@ -3,20 +3,183 @@ package handlers
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
q "platform/web/queries"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// region ListBill
|
||||
// PageBillByAdmin 分页查询全部账单
|
||||
func PageBillByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeBillRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ListBillReq struct {
|
||||
// 解析请求参数
|
||||
req := new(PageBillByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造查询条件
|
||||
do := q.Bill.Where()
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.TradeInnerNo != nil {
|
||||
do = do.Where(q.Trade.As("Trade").InnerNo.Eq(*req.TradeInnerNo))
|
||||
}
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.BillNo != nil {
|
||||
do = do.Where(q.Bill.BillNo.Eq(*req.BillNo))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Bill.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Bill.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
if req.ProductCode != nil {
|
||||
do = do.Where(q.Resource.As("Resource").Code.Eq(*req.ProductCode))
|
||||
}
|
||||
if req.SkuCode != nil {
|
||||
do = do.Where(q.Bill.
|
||||
Where(q.ResourceShort.As("Resource__Short").Code.Eq(*req.SkuCode)).
|
||||
Or(q.ResourceLong.As("Resource__Long").Code.Eq(*req.SkuCode)))
|
||||
}
|
||||
|
||||
// 查询用户列表
|
||||
list, total, err := q.Bill.
|
||||
Joins(
|
||||
q.Bill.User,
|
||||
q.Bill.Resource,
|
||||
q.Bill.Trade,
|
||||
q.Bill.Resource.Short,
|
||||
q.Bill.Resource.Long,
|
||||
).
|
||||
Select(
|
||||
q.Bill.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.Trade.As("Trade").InnerNo.As("Trade__inner_no"),
|
||||
q.Trade.As("Trade").Acquirer.As("Trade__acquirer"),
|
||||
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
|
||||
q.Resource.As("Resource").Type.As("Resource__type"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.Bill.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBillByAdminReq struct {
|
||||
core.PageReq
|
||||
BillNo *string `json:"bill_no"`
|
||||
Type *int `json:"type"`
|
||||
CreateAfter *time.Time `json:"create_after"`
|
||||
CreateBefore *time.Time `json:"create_before"`
|
||||
UserPhone *string `json:"user_phone,omitempty"`
|
||||
TradeInnerNo *string `json:"trade_inner_no,omitempty"`
|
||||
ResourceNo *string `json:"resource_no,omitempty"`
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
ProductCode *string `json:"product_code,omitempty"`
|
||||
SkuCode *string `json:"sku_code,omitempty"`
|
||||
}
|
||||
|
||||
// PageBillOfUserByAdmin 分页查询指定用户账单
|
||||
func PageBillOfUserByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeBillReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageBillOfUserByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构造查询条件
|
||||
do := q.Bill.Where(q.Bill.UserID.Eq(req.UserID))
|
||||
if req.TradeInnerNo != nil {
|
||||
do = do.Where(q.Trade.As("Trade").InnerNo.Eq(*req.TradeInnerNo))
|
||||
}
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.BillNo != nil {
|
||||
do = do.Where(q.Bill.BillNo.Eq(*req.BillNo))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Bill.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Bill.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
if req.ProductCode != nil {
|
||||
do = do.Where(q.Resource.As("Resource").Code.Eq(*req.ProductCode))
|
||||
}
|
||||
if req.SkuCode != nil {
|
||||
do = do.Where(q.Bill.
|
||||
Where(q.ResourceShort.As("Resource__Short").Code.Eq(*req.SkuCode)).
|
||||
Or(q.ResourceLong.As("Resource__Long").Code.Eq(*req.SkuCode)))
|
||||
}
|
||||
|
||||
// 查询账单列表
|
||||
list, total, err := q.Bill.
|
||||
Joins(
|
||||
q.Bill.Resource,
|
||||
q.Bill.Trade,
|
||||
q.Bill.Resource.Short,
|
||||
q.Bill.Resource.Long,
|
||||
).
|
||||
Select(
|
||||
q.Bill.ALL,
|
||||
q.Trade.As("Trade").InnerNo.As("Trade__inner_no"),
|
||||
q.Trade.As("Trade").Acquirer.As("Trade__acquirer"),
|
||||
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
|
||||
q.Resource.As("Resource").Type.As("Resource__type"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.Bill.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageBillOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
TradeInnerNo *string `json:"trade_inner_no,omitempty"`
|
||||
ResourceNo *string `json:"resource_no,omitempty"`
|
||||
BillNo *string `json:"bill_no,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
ProductCode *string `json:"product_code,omitempty"`
|
||||
SkuCode *string `json:"sku_code,omitempty"`
|
||||
}
|
||||
|
||||
// ListBill 获取账单列表
|
||||
@@ -41,10 +204,10 @@ func ListBill(c *fiber.Ctx) error {
|
||||
do.Where(q.Bill.Type.Eq(int(*req.Type)))
|
||||
}
|
||||
if req.CreateAfter != nil {
|
||||
do.Where(q.Bill.CreatedAt.Gte(*req.CreateAfter))
|
||||
do = do.Where(q.Bill.CreatedAt.Gte(req.CreateAfter.UTC()))
|
||||
}
|
||||
if req.CreateBefore != nil {
|
||||
do.Where(q.Bill.CreatedAt.Lte(*req.CreateBefore))
|
||||
do = do.Where(q.Bill.CreatedAt.Lte(req.CreateBefore.UTC()))
|
||||
}
|
||||
if req.BillNo != nil && *req.BillNo != "" {
|
||||
do.Where(q.Bill.BillNo.Eq(*req.BillNo))
|
||||
@@ -79,4 +242,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"`
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
"platform/web/globals/orm"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
s "platform/web/services"
|
||||
@@ -14,16 +15,8 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// region ListChannels
|
||||
|
||||
type ListChannelsReq struct {
|
||||
core.PageReq
|
||||
AuthType s.ChannelAuthType `json:"auth_type"`
|
||||
ExpireAfter *time.Time `json:"expire_after"`
|
||||
ExpireBefore *time.Time `json:"expire_before"`
|
||||
}
|
||||
|
||||
func ListChannels(c *fiber.Ctx) error {
|
||||
// ListChannel 分页查询当前用户通道
|
||||
func ListChannel(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authContext, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
@@ -49,15 +42,14 @@ func ListChannels(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if req.ExpireAfter != nil {
|
||||
cond.Where(q.Channel.ExpiredAt.Gte(*req.ExpireAfter))
|
||||
cond = cond.Where(q.Channel.ExpiredAt.Gte(req.ExpireAfter.UTC()))
|
||||
}
|
||||
if req.ExpireBefore != nil {
|
||||
cond.Where(q.Channel.ExpiredAt.Lte(*req.ExpireBefore))
|
||||
cond = cond.Where(q.Channel.ExpiredAt.Lte(req.ExpireBefore.UTC()))
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
channels, err := q.Channel.
|
||||
Preload(q.Channel.Proxy).
|
||||
Where(cond).
|
||||
Order(q.Channel.CreatedAt.Desc()).
|
||||
Offset(req.GetOffset()).
|
||||
@@ -87,29 +79,16 @@ func ListChannels(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region CreateChannel
|
||||
|
||||
type CreateChannelReq struct {
|
||||
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"`
|
||||
type ListChannelsReq struct {
|
||||
core.PageReq
|
||||
AuthType s.ChannelAuthType `json:"auth_type"`
|
||||
ExpireAfter *time.Time `json:"expire_after"`
|
||||
ExpireBefore *time.Time `json:"expire_before"`
|
||||
}
|
||||
|
||||
// CreateChannel 创建新通道
|
||||
func CreateChannel(c *fiber.Ctx) error {
|
||||
// 不检查权限,允许 api 调用
|
||||
|
||||
// 解析参数
|
||||
req := new(CreateChannelReq)
|
||||
@@ -123,49 +102,144 @@ func CreateChannel(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 创建通道
|
||||
no, err := s.FindResourceNoById(req.ResourceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
areaID, err := s.Area.FindIdByFilter(req.Prov, req.City)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter := &s.EdgeFilter{AreaID: areaID}
|
||||
if req.Isp != nil {
|
||||
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
|
||||
}
|
||||
result, err := s.Channel.CreateChannels(
|
||||
ip,
|
||||
req.ResourceId,
|
||||
no,
|
||||
req.AuthType == s.ChannelAuthTypeIp,
|
||||
req.AuthType == s.ChannelAuthTypePass,
|
||||
req.Count,
|
||||
s.EdgeFilter{
|
||||
Isp: u.ElseTo(req.Isp, m.ToEdgeISP),
|
||||
Prov: req.Prov,
|
||||
City: req.City,
|
||||
},
|
||||
filter,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
var resp = make([]*CreateChannelRespItem, len(result))
|
||||
for i, channel := range result {
|
||||
resp[i] = &CreateChannelRespItem{
|
||||
Proto: req.Protocol,
|
||||
Host: channel.Proxy.IP.String(),
|
||||
Port: channel.Port,
|
||||
}
|
||||
if req.AuthType == s.ChannelAuthTypePass {
|
||||
resp[i].Username = channel.Username
|
||||
resp[i].Password = channel.Password
|
||||
}
|
||||
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
|
||||
}
|
||||
|
||||
type CreateChannelReq struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
// CreateChannelV2 创建新通道 v2,使用 resource_no 替代 resource_id
|
||||
func CreateChannelV2(c *fiber.Ctx) error {
|
||||
// 不检查权限,允许 api 调用
|
||||
|
||||
// 解析参数
|
||||
req := new(CreateChannelReqV2)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return core.NewBizErr("解析参数失败", err)
|
||||
}
|
||||
return c.JSON(resp)
|
||||
|
||||
ip, err := netip.ParseAddr(c.IP())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取客户端地址失败", err)
|
||||
}
|
||||
|
||||
// 创建通道
|
||||
areaID, err := s.Area.FindIdByFilter(req.Prov, req.City)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter := &s.EdgeFilter{AreaID: areaID}
|
||||
if req.Isp != nil {
|
||||
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
|
||||
}
|
||||
result, err := s.Channel.CreateChannels(
|
||||
ip,
|
||||
req.ResourceNo,
|
||||
req.AuthType == s.ChannelAuthTypeIp,
|
||||
req.AuthType == s.ChannelAuthTypePass,
|
||||
req.Count,
|
||||
filter,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
|
||||
}
|
||||
|
||||
type CreateChannelResultType string
|
||||
|
||||
// endregion
|
||||
|
||||
// region RemoveChannels
|
||||
|
||||
type RemoveChannelsReq struct {
|
||||
Batch string `json:"batch" validate:"required"`
|
||||
Ids []int32 `json:"ids" validate:"required"`
|
||||
type CreateChannelReqV2 struct {
|
||||
ResourceNo string `json:"resource_no" 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"`
|
||||
}
|
||||
|
||||
// CreateChannelV3 创建新通道 v3,使用 resource_no + area_id
|
||||
func CreateChannelV3(c *fiber.Ctx) error {
|
||||
req := new(CreateChannelReqV3)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return core.NewBizErr("解析参数失败", err)
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(c.IP())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取客户端地址失败", err)
|
||||
}
|
||||
|
||||
filter := &s.EdgeFilter{AreaID: req.AreaID}
|
||||
if req.Isp != nil {
|
||||
filter.Isp = u.X(m.ToEdgeISP(*req.Isp))
|
||||
}
|
||||
result, err := s.Channel.CreateChannels(
|
||||
ip,
|
||||
req.ResourceNo,
|
||||
req.AuthType == s.ChannelAuthTypeIp,
|
||||
req.AuthType == s.ChannelAuthTypePass,
|
||||
req.Count,
|
||||
filter,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(buildCreateChannelResp(result, req.Protocol, req.AuthType))
|
||||
}
|
||||
|
||||
type CreateChannelReqV3 struct {
|
||||
ResourceNo string `json:"resource_no" validate:"required"`
|
||||
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
|
||||
Protocol int `json:"protocol" validate:"required"`
|
||||
Count int `json:"count" validate:"required"`
|
||||
AreaID *int32 `json:"area_id"`
|
||||
Isp *int `json:"isp"`
|
||||
}
|
||||
|
||||
type CreateChannelRespItem struct {
|
||||
Proto int `json:"-"`
|
||||
Host string `json:"host"`
|
||||
IP string `json:"ip"`
|
||||
Port uint16 `json:"port"`
|
||||
Username *string `json:"username,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// RemoveChannels 删除通道
|
||||
func RemoveChannels(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||
@@ -180,7 +254,7 @@ func RemoveChannels(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 删除通道
|
||||
err = s.Channel.RemoveChannels(req.Batch, req.Ids)
|
||||
err = s.Channel.RemoveChannels(req.Batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -188,4 +262,215 @@ func RemoveChannels(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
// endregion
|
||||
type RemoveChannelsReq struct {
|
||||
Batch string `json:"batch" validate:"required"`
|
||||
}
|
||||
|
||||
// PageChannelByAdmin 分页查询所有通道
|
||||
func PageChannelByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
var req PageChannelsByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
do := q.Channel.Where()
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.BatchNo != nil {
|
||||
do = do.Where(q.Channel.BatchNo.Eq(*req.BatchNo))
|
||||
}
|
||||
if req.ProxyHost != nil {
|
||||
do = do.Where(q.Channel.Host.Eq(*req.ProxyHost))
|
||||
}
|
||||
if req.ProxyPort != nil {
|
||||
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
|
||||
}
|
||||
if req.NodeIP != nil {
|
||||
ip, err := orm.ParseInet(*req.NodeIP)
|
||||
if err != nil {
|
||||
return core.NewBizErr("查询参数 ip 格式不正确")
|
||||
}
|
||||
do = do.Where(q.Channel.IP.Eq(ip))
|
||||
}
|
||||
if req.ExpiredAtStart != nil {
|
||||
do = do.Where(q.Channel.ExpiredAt.Gte(req.ExpiredAtStart.UTC()))
|
||||
}
|
||||
if req.ExpiredAtEnd != nil {
|
||||
do = do.Where(q.Channel.ExpiredAt.Lte(req.ExpiredAtEnd.UTC()))
|
||||
}
|
||||
if req.Expired != nil {
|
||||
if *req.Expired {
|
||||
do = do.Where(q.Channel.ExpiredAt.Lte(time.Now().UTC()))
|
||||
} else {
|
||||
do = do.Where(q.Channel.ExpiredAt.Gt(time.Now().UTC()))
|
||||
}
|
||||
}
|
||||
|
||||
// 查询通道列表
|
||||
list, total, err := q.Channel.
|
||||
Joins(q.Channel.User, q.Channel.Resource).
|
||||
Select(
|
||||
q.Channel.ALL,
|
||||
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
|
||||
q.Resource.As("Resource").Type.As("Resource__type"),
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.Channel.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageChannelsByAdminReq struct {
|
||||
core.PageReq
|
||||
UserPhone *string `json:"user_phone"`
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
BatchNo *string `json:"batch_no"`
|
||||
ProxyHost *string `json:"proxy_host"`
|
||||
ProxyPort *uint16 `json:"proxy_port"`
|
||||
NodeIP *string `json:"node_ip" validator:"omitempty,ip"`
|
||||
ExpiredAtStart *time.Time `json:"expired_at_start"`
|
||||
ExpiredAtEnd *time.Time `json:"expired_at_end"`
|
||||
Expired *bool `json:"expired"`
|
||||
}
|
||||
|
||||
// PageChannelOfUserByAdmin 分页查询指定用户的通道
|
||||
func PageChannelOfUserByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
var req PageChannelOfUserByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
do := q.Channel.Where(q.Channel.UserID.Eq(req.UserID))
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.As("Resource").ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.BatchNo != nil {
|
||||
do = do.Where(q.Channel.BatchNo.Eq(*req.BatchNo))
|
||||
}
|
||||
if req.ProxyHost != nil {
|
||||
do = do.Where(q.Channel.Host.Eq(*req.ProxyHost))
|
||||
}
|
||||
if req.ProxyPort != nil {
|
||||
do = do.Where(q.Channel.Port.Eq(*req.ProxyPort))
|
||||
}
|
||||
if req.ExpiredAtStart != nil {
|
||||
do = do.Where(q.Channel.ExpiredAt.Gte(req.ExpiredAtStart.UTC()))
|
||||
}
|
||||
if req.ExpiredAtEnd != nil {
|
||||
do = do.Where(q.Channel.ExpiredAt.Lte(req.ExpiredAtEnd.UTC()))
|
||||
}
|
||||
|
||||
// 查询通道列表
|
||||
list, total, err := q.Channel.
|
||||
Joins(q.Channel.User, q.Channel.Resource).
|
||||
Select(
|
||||
q.Channel.ALL,
|
||||
q.Resource.As("Resource").ResourceNo.As("Resource__resource_no"),
|
||||
q.Resource.As("Resource").Type.As("Resource__type"),
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.Channel.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageChannelOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
BatchNo *string `json:"batch_no"`
|
||||
ProxyHost *string `json:"proxy_host"`
|
||||
ProxyPort *uint16 `json:"proxy_port"`
|
||||
ExpiredAtStart *time.Time `json:"expired_at_start"`
|
||||
ExpiredAtEnd *time.Time `json:"expired_at_end"`
|
||||
}
|
||||
|
||||
// SyncChannelClearExpiredByAdmin 清理过期通道
|
||||
func SyncChannelClearExpiredByAdmin(c *fiber.Ctx) error {
|
||||
if _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelWriteClearExpired); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req SyncChannelClearExpiredByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count, err := s.Channel.ClearExpiredChannels(req.ProxyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(SyncChannelClearExpiredByAdminResp{
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
|
||||
type SyncChannelClearExpiredByAdminReq struct {
|
||||
ProxyID int32 `json:"proxy_id" validate:"required"`
|
||||
}
|
||||
|
||||
type SyncChannelClearExpiredByAdminResp struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func buildCreateChannelResp(result []*m.Channel, protocol int, authType s.ChannelAuthType) []*CreateChannelRespItem {
|
||||
resp := make([]*CreateChannelRespItem, len(result))
|
||||
for i, channel := range result {
|
||||
resp[i] = &CreateChannelRespItem{
|
||||
Proto: protocol,
|
||||
Host: channel.Host,
|
||||
IP: channel.Proxy.IP.String(),
|
||||
Port: channel.Port,
|
||||
}
|
||||
if authType == s.ChannelAuthTypePass {
|
||||
resp[i].Username = channel.Username
|
||||
resp[i].Password = channel.Password
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
129
web/handlers/coupon.go
Normal file
129
web/handlers/coupon.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func PageCouponByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.PageReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, total, err := s.Coupon.Page(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: list,
|
||||
})
|
||||
}
|
||||
|
||||
func AllCouponByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.Coupon.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func CreateCoupon(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateCouponData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Coupon.Create(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateCoupon(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateCouponData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Coupon.Update(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteCoupon(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Coupon.Delete(req.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AssignCoupon(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWriteAssign)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req AssignCouponReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Coupon.Assign(req.CouponID, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AssignCouponReq struct {
|
||||
CouponID int32 `json:"coupon_id" validate:"required"`
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
}
|
||||
329
web/handlers/coupon_user.go
Normal file
329
web/handlers/coupon_user.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
s "platform/web/services"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PageCouponUser 分页查询当前用户已发放优惠券
|
||||
func PageCouponUser(c *fiber.Ctx) error {
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageCouponUserReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conds := couponUserPageConditions(req.CouponUserPageFilter)
|
||||
conds = append(conds, q.CouponUser.UserID.Eq(authCtx.User.ID))
|
||||
|
||||
list, total, err := q.CouponUser.
|
||||
Joins(q.CouponUser.Coupon).
|
||||
Select(couponUserSelect(false)...).
|
||||
Where(conds...).
|
||||
Order(q.CouponUser.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageCouponUserReq struct {
|
||||
core.PageReq
|
||||
CouponUserPageFilter
|
||||
}
|
||||
|
||||
// GetCouponUser 获取当前用户已发放优惠券详情
|
||||
func GetCouponUser(c *fiber.Ctx) error {
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := q.CouponUser.
|
||||
Joins(q.CouponUser.Coupon).
|
||||
Select(couponUserSelect(false)...).
|
||||
Where(
|
||||
q.CouponUser.ID.Eq(req.Id),
|
||||
q.CouponUser.UserID.Eq(authCtx.User.ID),
|
||||
).
|
||||
Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return core.NewBizErr("已发放优惠券不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
return c.JSON(item)
|
||||
}
|
||||
|
||||
// PageCouponUserByAdmin 分页查询全部已发放优惠券
|
||||
func PageCouponUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageCouponUserByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conds := couponUserPageConditions(req.CouponUserPageFilter)
|
||||
if req.UserID != nil {
|
||||
conds = append(conds, q.CouponUser.UserID.Eq(*req.UserID))
|
||||
}
|
||||
if req.UserPhone != nil {
|
||||
conds = append(conds, q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
|
||||
list, total, err := q.CouponUser.
|
||||
Joins(q.CouponUser.Coupon, q.CouponUser.User).
|
||||
Select(couponUserSelect(true)...).
|
||||
Where(conds...).
|
||||
Order(q.CouponUser.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageCouponUserByAdminReq struct {
|
||||
core.PageReq
|
||||
CouponUserPageFilter
|
||||
UserID *int32 `json:"user_id,omitempty"`
|
||||
UserPhone *string `json:"user_phone,omitempty"`
|
||||
}
|
||||
|
||||
// PageCouponUserOfUserByAdmin 分页查询指定用户已发放优惠券
|
||||
func PageCouponUserOfUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageCouponUserOfUserByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conds := couponUserPageConditions(req.CouponUserPageFilter)
|
||||
conds = append(conds, q.CouponUser.UserID.Eq(req.UserID))
|
||||
|
||||
list, total, err := q.CouponUser.
|
||||
Joins(q.CouponUser.Coupon, q.CouponUser.User).
|
||||
Select(couponUserSelect(true)...).
|
||||
Where(conds...).
|
||||
Order(q.CouponUser.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageCouponUserOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
CouponUserPageFilter
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
}
|
||||
|
||||
// GetCouponUserByAdmin 获取已发放优惠券详情
|
||||
func GetCouponUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := q.CouponUser.
|
||||
Joins(q.CouponUser.Coupon, q.CouponUser.User).
|
||||
Select(couponUserSelect(true)...).
|
||||
Where(q.CouponUser.ID.Eq(req.Id)).
|
||||
Take()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return core.NewBizErr("已发放优惠券不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return core.NewBizErr("获取数据失败", err)
|
||||
}
|
||||
|
||||
return c.JSON(item)
|
||||
}
|
||||
|
||||
func CreateCouponUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateCouponUserData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.CouponUser.Create(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateCouponUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateCouponUserData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.CouponUser.Update(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteCouponUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.CouponUser.Delete(req.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CouponUserPageFilter struct {
|
||||
CouponID *int32 `json:"coupon_id,omitempty"`
|
||||
CouponName *string `json:"coupon_name,omitempty"`
|
||||
Status *m.CouponUserStatus `json:"status,omitempty"`
|
||||
Expired *bool `json:"expired,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
ExpireAtStart *time.Time `json:"expire_at_start,omitempty"`
|
||||
ExpireAtEnd *time.Time `json:"expire_at_end,omitempty"`
|
||||
UsedAtStart *time.Time `json:"used_at_start,omitempty"`
|
||||
UsedAtEnd *time.Time `json:"used_at_end,omitempty"`
|
||||
}
|
||||
|
||||
func couponUserPageConditions(req CouponUserPageFilter) []gen.Condition {
|
||||
conds := make([]gen.Condition, 0)
|
||||
if req.CouponID != nil {
|
||||
conds = append(conds, q.CouponUser.CouponID.Eq(*req.CouponID))
|
||||
}
|
||||
if req.CouponName != nil {
|
||||
conds = append(conds, q.Coupon.As("Coupon").Name.Like("%"+*req.CouponName+"%"))
|
||||
}
|
||||
if req.Status != nil {
|
||||
conds = append(conds, q.CouponUser.Status.Eq(int(*req.Status)))
|
||||
}
|
||||
if req.Expired != nil {
|
||||
if *req.Expired {
|
||||
conds = append(conds, q.CouponUser.ExpireAt.IsNotNull(), q.CouponUser.ExpireAt.Lte(time.Now().UTC()))
|
||||
} else {
|
||||
conds = append(conds, q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now().UTC())))
|
||||
}
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
conds = append(conds, q.CouponUser.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
conds = append(conds, q.CouponUser.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
if req.ExpireAtStart != nil {
|
||||
conds = append(conds, q.CouponUser.ExpireAt.Gte(req.ExpireAtStart.UTC()))
|
||||
}
|
||||
if req.ExpireAtEnd != nil {
|
||||
conds = append(conds, q.CouponUser.ExpireAt.Lte(req.ExpireAtEnd.UTC()))
|
||||
}
|
||||
if req.UsedAtStart != nil {
|
||||
conds = append(conds, q.CouponUser.UsedAt.Gte(req.UsedAtStart.UTC()))
|
||||
}
|
||||
if req.UsedAtEnd != nil {
|
||||
conds = append(conds, q.CouponUser.UsedAt.Lte(req.UsedAtEnd.UTC()))
|
||||
}
|
||||
return conds
|
||||
}
|
||||
|
||||
func couponUserSelect(includeUser bool) []field.Expr {
|
||||
cols := []field.Expr{
|
||||
q.CouponUser.ALL,
|
||||
q.Coupon.As("Coupon").ID.As("Coupon__id"),
|
||||
q.Coupon.As("Coupon").Name.As("Coupon__name"),
|
||||
q.Coupon.As("Coupon").Amount.As("Coupon__amount"),
|
||||
q.Coupon.As("Coupon").MinAmount.As("Coupon__min_amount"),
|
||||
q.Coupon.As("Coupon").Count_.As("Coupon__count"),
|
||||
q.Coupon.As("Coupon").Status.As("Coupon__status"),
|
||||
q.Coupon.As("Coupon").ExpireType.As("Coupon__expire_type"),
|
||||
q.Coupon.As("Coupon").ExpireAt.As("Coupon__expire_at"),
|
||||
q.Coupon.As("Coupon").ExpireIn.As("Coupon__expire_in"),
|
||||
q.Coupon.As("Coupon").CreatedAt.As("Coupon__created_at"),
|
||||
q.Coupon.As("Coupon").UpdatedAt.As("Coupon__updated_at"),
|
||||
}
|
||||
|
||||
if includeUser {
|
||||
cols = append(cols,
|
||||
q.User.As("User").ID.As("User__id"),
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
)
|
||||
}
|
||||
|
||||
return cols
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type RegisterEdgeReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Version int `json:"version" validate:"required"`
|
||||
}
|
||||
|
||||
type RegisterEdgeResp struct {
|
||||
Id int32 `json:"id"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
func AssignEdge(c *fiber.Ctx) (err error) {
|
||||
return c.JSON(map[string]any{
|
||||
"error": "接口暂不可用",
|
||||
})
|
||||
// // 验证请求参数
|
||||
// var req = new(RegisterEdgeReq)
|
||||
// err = g.Validator.Validate(c, req)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 全局锁,防止并发注册
|
||||
// var mutex = g.Redsync.NewMutex("edge:discovery")
|
||||
// if err := mutex.Lock(); err != nil {
|
||||
// return errors.New("服务繁忙,请稍后重试")
|
||||
// }
|
||||
// defer func() {
|
||||
// if ok, err := mutex.Unlock(); err != nil {
|
||||
// slog.Error("解锁失败", slog.Bool("ok", ok), slog.Any("err", err))
|
||||
// }
|
||||
// }()
|
||||
|
||||
// // 检查节点
|
||||
// var fwd *m.Proxy
|
||||
// var edge *m.Edge
|
||||
// edge, err = q.Edge.
|
||||
// Where(q.Edge.Mac.Eq(req.Name)).
|
||||
// Take()
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// // 挑选合适的转发服务
|
||||
// fwd, err = q.Proxy.
|
||||
// LeftJoin(q.Edge, q.Edge.ProxyID.EqCol(q.Proxy.ID), q.Edge.Status.Eq(1)).
|
||||
// Select(q.Proxy.ALL, q.Edge.ALL.Count().As("count")).
|
||||
// Where(q.Proxy.Type.Eq(int32(proxy2.TypeSelfHosted))).
|
||||
// Group(q.Proxy.ID).
|
||||
// Order(field.NewField("", "count").Desc()).
|
||||
// First()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // 保存节点信息
|
||||
// edge = &m.Edge{
|
||||
// Name: req.Name,
|
||||
// Version: int32(req.Version),
|
||||
// Type: int32(edge2.TypeSelfHosted),
|
||||
// ProxyID: &fwd.ID,
|
||||
// }
|
||||
// err = q.Edge.Create(edge)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// } else if err == nil {
|
||||
// // 获取已配置的转发服务
|
||||
// fwd, err = q.Proxy.
|
||||
// Where(q.Proxy.ID.Eq(*edge.ProxyID)).
|
||||
// Take()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // 节点已存在,更新版本号
|
||||
// if edge.Version < int32(req.Version) {
|
||||
// _, err = q.Edge.
|
||||
// Where(q.Edge.ID.Eq(edge.ID)).
|
||||
// UpdateSimple(q.Edge.Version.Value(int32(req.Version)))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 返回服务地址
|
||||
// return c.JSON(RegisterEdgeResp{
|
||||
// Id: edge.ID,
|
||||
// Host: fwd.Host,
|
||||
// })
|
||||
}
|
||||
|
||||
type AllEdgesAvailableReq struct {
|
||||
s.EdgeFilter
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type AllEdgesAvailableRespItem struct {
|
||||
Ip string `json:"ip"` // 节点地址
|
||||
Port int32 `json:"port"` // 代理服务端口
|
||||
Isp string `json:"isp"` // 运营商
|
||||
Prov string `json:"prov"` // 省份
|
||||
City string `json:"city"` // 城市
|
||||
Status int32 `json:"status"` // 节点状态:0-离线,1-正常
|
||||
}
|
||||
|
||||
func AllEdgesAvailable(c *fiber.Ctx) (err error) {
|
||||
return c.JSON(map[string]any{
|
||||
"error": "接口暂不可用",
|
||||
})
|
||||
|
||||
// // 检查权限
|
||||
// _, err = auth.GetAuthCtx(c).PermitSecretClient()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 验证请求参数
|
||||
// var req = new(AllEdgesAvailableReq)
|
||||
// err = g.Validator.Validate(c, req)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 获取可用的转发服务
|
||||
// infos, err := s.Edge.AllEdges(req.Count, req.EdgeFilter)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 返回结果
|
||||
// var edges = make([]AllEdgesAvailableRespItem, len(infos))
|
||||
// for i, info := range infos {
|
||||
// edges[i] = AllEdgesAvailableRespItem{
|
||||
// Ip: info.Host,
|
||||
// Port: u.Z(info.ProxyPort),
|
||||
// Isp: edge2.ISP(info.Isp).String(),
|
||||
// Prov: info.Prov,
|
||||
// City: info.City,
|
||||
// Status: info.Status,
|
||||
// }
|
||||
// }
|
||||
// return c.JSON(edges)
|
||||
}
|
||||
@@ -18,19 +18,7 @@ import (
|
||||
jdclient "github.com/jdcloud-api/jdcloud-sdk-go/services/cloudauth/client"
|
||||
)
|
||||
|
||||
// region Identify
|
||||
|
||||
type IdentifyReq struct {
|
||||
Type int `json:"type" validate:"required,oneof=1 2"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
IdenNo string `json:"iden_no" validate:"required"`
|
||||
}
|
||||
|
||||
type IdentifyRes struct {
|
||||
Identified bool `json:"identified"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
// Identify 发起实名认证
|
||||
func Identify(c *fiber.Ctx) error {
|
||||
|
||||
// 检查权限
|
||||
@@ -99,7 +87,21 @@ func Identify(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// endregion
|
||||
type IdentifyReq struct {
|
||||
Type int `json:"type" validate:"required,oneof=1 2"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
IdenNo string `json:"iden_no" validate:"required"`
|
||||
}
|
||||
|
||||
type IdentifyRes struct {
|
||||
Identified bool `json:"identified"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type idenResultData struct {
|
||||
Success bool
|
||||
Message string
|
||||
}
|
||||
|
||||
// IdentifyCallbackNew 更新用户实名认证状态
|
||||
func IdentifyCallbackNew(c *fiber.Ctx) error {
|
||||
@@ -110,18 +112,17 @@ func IdentifyCallbackNew(c *fiber.Ctx) error {
|
||||
Success bool `json:"success" validate:"required"`
|
||||
})
|
||||
if err := c.QueryParser(req); err != nil {
|
||||
return core.NewBizErr("解析请求参数失败", err)
|
||||
return renderIdenResult(c, false, "解析请求参数失败")
|
||||
}
|
||||
|
||||
// 获取 token
|
||||
infoStr, err := g.Redis.GetDel(c.Context(), idenKey(req.Id)).Bytes()
|
||||
if err != nil {
|
||||
return core.NewBizErr("实名认证状态已失效", err)
|
||||
return renderIdenResult(c, false, "实名认证状态已失效,请重新发起认证")
|
||||
}
|
||||
info := idenInfo{}
|
||||
err = json.Unmarshal(infoStr, &info)
|
||||
if err != nil {
|
||||
return core.NewServErr("解析实名认证信息失败", err)
|
||||
if err = json.Unmarshal(infoStr, &info); err != nil {
|
||||
return renderIdenResult(c, false, "解析实名认证信息失败,请重新发起认证")
|
||||
}
|
||||
|
||||
// 获取认证结果
|
||||
@@ -131,17 +132,17 @@ func IdentifyCallbackNew(c *fiber.Ctx) error {
|
||||
info.Token,
|
||||
))
|
||||
if err != nil {
|
||||
return core.NewServErr("获取实名认证结果失败", err)
|
||||
return renderIdenResult(c, false, "获取实名认证结果失败,请重新发起认证")
|
||||
}
|
||||
if resp.Error.Code != 0 {
|
||||
return core.NewServErr(fmt.Sprintf("获取实名认证结果失败: %s", resp.Error.Message))
|
||||
return renderIdenResult(c, false, fmt.Sprintf("获取实名认证结果失败:%s", resp.Error.Message))
|
||||
}
|
||||
if resp.Result.H5Result != "ok" || resp.Result.SmResult != "ok" || resp.Result.RxResult != "ok" {
|
||||
return core.NewBizErr(fmt.Sprintf("实名认证失败: %s", resp.Result.Desc))
|
||||
return renderIdenResult(c, false, fmt.Sprintf("实名认证未通过:%s", resp.Result.Desc))
|
||||
}
|
||||
|
||||
// 更新用户实名认证状态
|
||||
_, err = q.User.Debug().
|
||||
r, err := q.User.
|
||||
Where(q.User.ID.Eq(info.Uid)).
|
||||
UpdateSimple(
|
||||
q.User.IDType.Value(info.Type),
|
||||
@@ -150,11 +151,47 @@ func IdentifyCallbackNew(c *fiber.Ctx) error {
|
||||
q.User.IDToken.Value(info.Token),
|
||||
)
|
||||
if err != nil {
|
||||
return core.NewServErr("更新用户实名信息失败", err)
|
||||
return renderIdenResult(c, false, "保存实名认证信息失败,请联系客服处理")
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return renderIdenResult(c, false, "用户状态已失效")
|
||||
}
|
||||
|
||||
// 返回结果页面
|
||||
return c.SendString("🎉认证成功!现在可以安全关闭这个页面")
|
||||
return renderIdenResult(c, true, "实名认证成功,请在扫码页面点击按钮完成认证")
|
||||
}
|
||||
|
||||
func renderIdenResult(c *fiber.Ctx, success bool, message string) error {
|
||||
return c.Render("views/iden-result", idenResultData{
|
||||
Success: success,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// DebugIdentifyClear 清除用户实名认证状态(调试用)
|
||||
func DebugIdentifyClear(c *fiber.Ctx) error {
|
||||
phone := c.Params("phone")
|
||||
if phone == "" {
|
||||
return core.NewServErr("需要提供手机号")
|
||||
}
|
||||
|
||||
r, err := q.User.
|
||||
Where(
|
||||
q.User.Phone.Eq(phone),
|
||||
).
|
||||
UpdateSimple(
|
||||
q.User.IDType.Value(0),
|
||||
q.User.IDNo.Value(""),
|
||||
q.User.IDToken.Value(""),
|
||||
)
|
||||
if err != nil {
|
||||
return core.NewServErr("清除实名认证失败")
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return core.NewServErr("用户状态已失效")
|
||||
}
|
||||
|
||||
return c.SendString("实名信息已清除")
|
||||
}
|
||||
|
||||
func idenKey(id string) string {
|
||||
|
||||
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
|
||||
53
web/handlers/permission.go
Normal file
53
web/handlers/permission.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func AllPermissionByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopePermissionRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.Permission.ListPermissions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func PagePermissionByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopePermissionRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PagePermissionByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取权限列表
|
||||
list, total, err := s.Permission.PagePermissions(req.PageReq)
|
||||
if err != nil {
|
||||
return core.NewServErr("获取权限列表失败")
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
}
|
||||
|
||||
type PagePermissionByAdminReq struct {
|
||||
core.PageReq
|
||||
}
|
||||
255
web/handlers/product.go
Normal file
255
web/handlers/product.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func AllProductByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
// var req AllProductsByAdminReq
|
||||
// if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// 查询产品
|
||||
products, err := s.Product.AllProducts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(products)
|
||||
}
|
||||
|
||||
type AllProductsByAdminReq struct {
|
||||
}
|
||||
|
||||
func AllProduct(c *fiber.Ctx) error {
|
||||
infos, err := s.Product.AllProductSaleInfos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(infos)
|
||||
}
|
||||
|
||||
func CreateProduct(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateProductData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Product.CreateProduct(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateProduct(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateProductData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Product.UpdateProduct(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteProduct(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Product.DeleteProduct(req.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AllProductSkuByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductSkuRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req AllProductSkuByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.ProductSku.All(req.Code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
type AllProductSkuByAdminReq struct {
|
||||
Code string `json:"product_code"`
|
||||
}
|
||||
|
||||
func PageProductSkuByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductSkuRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageProductSkuByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, total, err := s.ProductSku.Page(&req.PageReq, req.ProductId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: list,
|
||||
})
|
||||
}
|
||||
|
||||
type PageProductSkuByAdminReq struct {
|
||||
core.PageReq
|
||||
ProductId *int32 `json:"product_id"`
|
||||
}
|
||||
|
||||
func CreateProductSku(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductSkuWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateProductSkuData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductSku.Create(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateProductSku(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductSkuWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateProductSkuData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductSku.Update(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateProductStatusSku(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductSkuWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
ID int32 `json:"id"`
|
||||
Status int32 `json:"status"`
|
||||
}
|
||||
var req Params
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductSku.Update(s.UpdateProductSkuData{
|
||||
ID: req.ID,
|
||||
Status: &req.Status,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func BatchUpdateProductSkuDiscount(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductSkuWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.BatchUpdateSkuDiscountData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductSku.BatchUpdateDiscount(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteProductSku(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductSkuWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductSku.Delete(req.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
105
web/handlers/product_discount.go
Normal file
105
web/handlers/product_discount.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func PageDiscountByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeDiscountRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.PageReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, total, err := s.ProductDiscount.Page(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: list,
|
||||
})
|
||||
}
|
||||
|
||||
func AllDiscountByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeDiscountRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list, err := s.ProductDiscount.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func CreateDiscount(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeDiscountWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateProductDiscountData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductDiscount.Create(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateDiscount(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeDiscountWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateProductDiscountData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductDiscount.Update(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteDiscount(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeDiscountWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ProductDiscount.Delete(req.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,379 +1,160 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"platform/pkg/env"
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
s "platform/web/services"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func DebugRegisterProxyBaiYin(c *fiber.Ctx) error {
|
||||
if env.RunMode != env.RunModeDev {
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
// ====================
|
||||
// admin 路由
|
||||
// ====================
|
||||
|
||||
ok, err := g.Redis.SetNX(c.Context(), "debug:channel:register:127.0.0.1", true, 0).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("注册代理", "ok", ok)
|
||||
if !ok {
|
||||
return fiber.ErrConflict
|
||||
}
|
||||
|
||||
err = s.Proxy.RegisterBaiyin("1a:2b:3c:4d:5e:6f", netip.AddrFrom4([4]byte{127, 0, 0, 1}), "test", "test")
|
||||
func PageProxyByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
var req core.PageReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// region 报告上线
|
||||
list, total, err := s.Proxy.Page(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ProxyReportOnlineReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Version int `json:"version" validate:"required"`
|
||||
}
|
||||
|
||||
type ProxyReportOnlineResp struct {
|
||||
Id int32 `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
Permits []*ProxyPermit `json:"permits"`
|
||||
Edges []*ProxyEdge `json:"edges"`
|
||||
}
|
||||
|
||||
func ProxyReportOnline(c *fiber.Ctx) (err error) {
|
||||
return c.JSON(map[string]any{
|
||||
"error": "接口暂不可用",
|
||||
return c.JSON(core.PageResp{
|
||||
List: list,
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
})
|
||||
|
||||
// // 检查接口权限
|
||||
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 验证请求参数
|
||||
// var req = new(ProxyReportOnlineReq)
|
||||
// err = g.Validator.Validate(c, req)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 创建代理
|
||||
// var ip = c.Context().RemoteIP()
|
||||
|
||||
// var secretBytes = make([]byte, 16)
|
||||
// if _, err := rand.Read(secretBytes); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// var secret = base32.StdEncoding.
|
||||
// WithPadding(base32.NoPadding).
|
||||
// EncodeToString(secretBytes)
|
||||
// slog.Debug("生成随机密钥", "ip", ip, "secret", secret)
|
||||
|
||||
// var proxy = &m.Proxy{
|
||||
// Mac: req.Name,
|
||||
// Version: int32(req.Version),
|
||||
// Type: m.ProxyTypeSelfHosted,
|
||||
// IP: ip,
|
||||
// Secret: &secret,
|
||||
// Status: 1,
|
||||
// }
|
||||
// err = q.Proxy.
|
||||
// Clauses(clause.OnConflict{
|
||||
// UpdateAll: true,
|
||||
// Columns: []clause.Column{
|
||||
// {Name: q.Proxy.Mac.ColumnName().String()},
|
||||
// },
|
||||
// }).
|
||||
// Create(proxy)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 获取边缘节点信息
|
||||
// data, err := q.Edge.Where(
|
||||
// q.Edge.ProxyID.Eq(proxy.ID),
|
||||
// ).Find()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// edges := make([]*ProxyEdge, len(data))
|
||||
// for i, edge := range data {
|
||||
// edges[i] = &ProxyEdge{
|
||||
// Id: edge.ID,
|
||||
// Port: edge.ProxyPort,
|
||||
// Prov: &edge.Prov,
|
||||
// City: &edge.City,
|
||||
// Isp: u.P(edge2.ISP(edge.Isp).String()),
|
||||
// Status: &edge.Status,
|
||||
// Loss: edge.Loss,
|
||||
// Rtt: edge.Rtt,
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 获取许可配置
|
||||
// channels, err := q.Channel.Where(
|
||||
// q.Channel.ProxyID.Eq(proxy.ID),
|
||||
// q.Channel.Expiration.Gt(orm.LocalDateTime(time.Now())),
|
||||
// ).Find()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// var permits = make([]*ProxyPermit, len(channels))
|
||||
// for i, channel := range channels {
|
||||
// if channel.EdgeID == nil {
|
||||
// return core.NewBizErr(fmt.Sprintf("权限解析异常,通道缺少边缘节点ID %d", channel.ID))
|
||||
// }
|
||||
// permits[i] = &ProxyPermit{
|
||||
// Id: *channel.EdgeID,
|
||||
// Expire: time.Time(channel.Expiration),
|
||||
// Whitelists: u.P(strings.Split(u.Z(channel.Whitelists), ",")),
|
||||
// Username: channel.Username,
|
||||
// Password: channel.Password,
|
||||
// }
|
||||
// }
|
||||
|
||||
// slog.Debug("注册转发服务", "ip", ip, "id", proxy.ID)
|
||||
// return c.JSON(&ProxyReportOnlineResp{
|
||||
// Id: proxy.ID,
|
||||
// Secret: secret,
|
||||
// Edges: edges,
|
||||
// Permits: permits,
|
||||
// })
|
||||
}
|
||||
|
||||
// region 报告下线
|
||||
func AllProxyByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ProxyReportOfflineReq struct {
|
||||
Id int32 `json:"id" validate:"required"`
|
||||
list, err := s.Proxy.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(list)
|
||||
}
|
||||
|
||||
func ProxyReportOffline(c *fiber.Ctx) (err error) {
|
||||
return c.JSON(map[string]any{
|
||||
"error": "接口暂不可用",
|
||||
})
|
||||
func CreateProxy(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // 检查接口权限
|
||||
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
var req s.CreateProxy
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // 验证请求参数
|
||||
// var req = new(ProxyReportOfflineReq)
|
||||
// err = g.Validator.Validate(c, req)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
if err := s.Proxy.Create(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // 下线转发服务
|
||||
// _, err = q.Proxy.
|
||||
// Where(q.Proxy.ID.Eq(req.Id)).
|
||||
// UpdateSimple(q.Proxy.Status.Value(0))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 下线所有相关的边缘节点
|
||||
// _, err = q.Edge.
|
||||
// Where(q.Edge.ProxyID.Eq(req.Id)).
|
||||
// UpdateSimple(q.Edge.Status.Value(0))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return nil
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
// region 报告更新
|
||||
func UpdateProxy(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ProxyReportUpdateReq struct {
|
||||
Id int32 `json:"id" validate:"required"`
|
||||
Edges []*ProxyEdge `json:"edges" validate:"required"`
|
||||
var req s.UpdateProxy
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Proxy.Update(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
func ProxyReportUpdate(c *fiber.Ctx) (err error) {
|
||||
return c.JSON(map[string]any{
|
||||
"error": "接口暂不可用",
|
||||
})
|
||||
func UpdateProxyStatus(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWriteStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // 检查接口权限
|
||||
// _, err = auth2.GetAuthCtx(c).PermitSecretClient()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
var req s.UpdateProxyStatus
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // 验证请求参数
|
||||
// var req = new(ProxyReportUpdateReq)
|
||||
// err = g.Validator.Validate(c, req)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
if err := s.Proxy.UpdateStatus(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // 更新节点信息
|
||||
// var idsActive = make([]int32, 0, len(req.Edges))
|
||||
// var idsInactive = make([]int32, 0, len(req.Edges))
|
||||
// var idsIspUnknown = make([]int32, 0, len(req.Edges))
|
||||
// var idsIspTelecom = make([]int32, 0, len(req.Edges))
|
||||
// var idsIspUnicom = make([]int32, 0, len(req.Edges))
|
||||
// var idsIspMobile = make([]int32, 0, len(req.Edges))
|
||||
// var otherEdges = make([]*ProxyEdge, 0, len(req.Edges))
|
||||
// for _, edge := range req.Edges {
|
||||
|
||||
// // 检查更新ISP
|
||||
// if edge.Isp != nil {
|
||||
// switch edge2.ISPFromStr(*edge.Isp) {
|
||||
// case edge2.IspUnknown:
|
||||
// idsIspUnknown = append(idsIspUnknown, edge.Id)
|
||||
// case edge2.IspChinaTelecom:
|
||||
// idsIspTelecom = append(idsIspTelecom, edge.Id)
|
||||
// case edge2.IspChinaUnicom:
|
||||
// idsIspUnicom = append(idsIspUnicom, edge.Id)
|
||||
// case edge2.IspChinaMobile:
|
||||
// idsIspMobile = append(idsIspMobile, edge.Id)
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 检查更新状态
|
||||
// if edge.Status != nil {
|
||||
// if *edge.Status == 1 {
|
||||
// idsActive = append(idsActive, edge.Id)
|
||||
// } else {
|
||||
// idsInactive = append(idsInactive, edge.Id)
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 无法分类更新
|
||||
// if edge.Host != nil || edge.Port != nil || edge.Prov != nil || edge.City != nil {
|
||||
// otherEdges = append(otherEdges, edge)
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
|
||||
// slog.Debug("更新边缘节点信息",
|
||||
// "active", len(idsActive),
|
||||
// "inactive", len(idsInactive),
|
||||
// "isp_unknown", len(idsIspUnknown),
|
||||
// "isp_telecom", len(idsIspTelecom),
|
||||
// "isp_unicom", len(idsIspUnicom),
|
||||
// "isp_mobile", len(idsIspMobile),
|
||||
// "other_edges", len(otherEdges),
|
||||
// )
|
||||
|
||||
// err = q.Q.Transaction(func(q *q.Query) error {
|
||||
// // 更新边缘节点状态
|
||||
// if len(idsActive) > 0 {
|
||||
// _, err = q.Edge.Debug().
|
||||
// Where(q.Edge.ID.In(idsActive...)).
|
||||
// UpdateSimple(q.Edge.Status.Value(1))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// if len(idsInactive) > 0 {
|
||||
// _, err = q.Edge.Debug().
|
||||
// Where(q.Edge.ID.In(idsInactive...)).
|
||||
// UpdateSimple(q.Edge.Status.Value(0))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 更新边缘节点ISP
|
||||
// if len(idsIspUnknown) > 0 {
|
||||
// _, err = q.Edge.Debug().
|
||||
// Where(q.Edge.ID.In(idsIspUnknown...)).
|
||||
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspUnknown)))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// if len(idsIspTelecom) > 0 {
|
||||
// _, err = q.Edge.Debug().
|
||||
// Where(q.Edge.ID.In(idsIspTelecom...)).
|
||||
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaTelecom)))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// if len(idsIspUnicom) > 0 {
|
||||
// _, err = q.Edge.Debug().
|
||||
// Where(q.Edge.ID.In(idsIspUnicom...)).
|
||||
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaUnicom)))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// if len(idsIspMobile) > 0 {
|
||||
// _, err = q.Edge.Debug().
|
||||
// Where(q.Edge.ID.In(idsIspMobile...)).
|
||||
// UpdateSimple(q.Edge.Isp.Value(int32(edge2.IspChinaMobile)))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 更新其他边缘节点信息
|
||||
// for _, edge := range otherEdges {
|
||||
// do := q.Edge.Debug().Where(q.Edge.ID.Eq(edge.Id))
|
||||
|
||||
// var assigns = make([]field.AssignExpr, 0, 5)
|
||||
// if edge.Host != nil {
|
||||
// assigns = append(assigns, q.Edge.Host.Value(*edge.Host))
|
||||
// }
|
||||
// if edge.Port != nil {
|
||||
// assigns = append(assigns, q.Edge.ProxyPort.Value(*edge.Port))
|
||||
// }
|
||||
// if edge.Prov != nil {
|
||||
// assigns = append(assigns, q.Edge.Prov.Value(*edge.Prov))
|
||||
// }
|
||||
// if edge.City != nil {
|
||||
// assigns = append(assigns, q.Edge.City.Value(*edge.City))
|
||||
// }
|
||||
|
||||
// // 更新边缘节点
|
||||
// _, err := do.UpdateSimple(assigns...)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("更新边缘节点 %d 失败: %w", edge.Id, err)
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// })
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return nil
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
type ProxyPermit struct {
|
||||
Id int32 `json:"id"`
|
||||
Expire time.Time `json:"expire"`
|
||||
Whitelists *[]string `json:"whitelists"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
func SyncProxyPorts(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Proxy.SyncPorts(req.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
type ProxyEdge struct {
|
||||
Id int32 `json:"id"`
|
||||
Host *string `json:"host,omitempty"` // 边缘节点地址
|
||||
Port *int32 `json:"port,omitempty"` // 边缘节点代理端口
|
||||
Prov *string `json:"prov,omitempty"`
|
||||
City *string `json:"city,omitempty"`
|
||||
Isp *string `json:"isp,omitempty"`
|
||||
Status *int32 `json:"status,omitempty"`
|
||||
Loss *int32 `json:"loss,omitempty"` // 丢包率
|
||||
Rtt *int32 `json:"latency,omitempty"` // 延迟
|
||||
func SyncProxyChains(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Proxy.SyncChains(req.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
func RemoveProxy(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Proxy.Remove(req.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
@@ -15,18 +15,8 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ListResourceShortReq struct {
|
||||
core.PageReq
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
Active *bool `json:"active"`
|
||||
Type *int `json:"type"`
|
||||
CreateAfter *time.Time `json:"create_after"`
|
||||
CreateBefore *time.Time `json:"create_before"`
|
||||
ExpireAfter *time.Time `json:"expire_after"`
|
||||
ExpireBefore *time.Time `json:"expire_before"`
|
||||
}
|
||||
|
||||
func ListResourceShort(c *fiber.Ctx) error {
|
||||
// PageResourceShort 分页查询当前用户短效套餐
|
||||
func PageResourceShort(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -54,16 +44,29 @@ func ListResourceShort(c *fiber.Ctx) error {
|
||||
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Type.Eq(*req.Type))
|
||||
}
|
||||
if req.CreateAfter != nil {
|
||||
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter))
|
||||
do = do.Where(q.Resource.CreatedAt.Gte(req.CreateAfter.UTC()))
|
||||
}
|
||||
if req.CreateBefore != nil {
|
||||
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
||||
do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
|
||||
}
|
||||
if req.ExpireAfter != nil {
|
||||
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Gte(*req.ExpireAfter))
|
||||
do = do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(req.ExpireAfter.UTC()))
|
||||
}
|
||||
if req.ExpireBefore != nil {
|
||||
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Lte(*req.ExpireBefore))
|
||||
do = do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(req.ExpireBefore.UTC()))
|
||||
}
|
||||
if req.Status != nil {
|
||||
var short = q.ResourceShort.As(q.Resource.Short.Name())
|
||||
switch *req.Status {
|
||||
case 1:
|
||||
var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Gte(time.Now().UTC()))
|
||||
var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.GtCol(short.Used))
|
||||
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
|
||||
case 2:
|
||||
var timeCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeTime)), short.ExpireAt.Lte(time.Now().UTC()))
|
||||
var quotaCond = q.Resource.Where(short.Type.Eq(int(m.ResourceModeQuota)), short.Quota.LteCol(short.Used))
|
||||
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
|
||||
}
|
||||
}
|
||||
|
||||
resource, err := q.Resource.Where(do).
|
||||
@@ -81,6 +84,7 @@ func ListResourceShort(c *fiber.Ctx) error {
|
||||
total = int64(len(resource) + req.GetOffset())
|
||||
} else {
|
||||
total, err = q.Resource.
|
||||
Joins(q.Resource.Short).
|
||||
Where(do).
|
||||
Count()
|
||||
if err != nil {
|
||||
@@ -96,7 +100,7 @@ func ListResourceShort(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
type ListResourceLongReq struct {
|
||||
type PageResourceShortReq struct {
|
||||
core.PageReq
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
Active *bool `json:"active"`
|
||||
@@ -105,9 +109,11 @@ type ListResourceLongReq struct {
|
||||
CreateBefore *time.Time `json:"create_before"`
|
||||
ExpireAfter *time.Time `json:"expire_after"`
|
||||
ExpireBefore *time.Time `json:"expire_before"`
|
||||
Status *int `json:"status"` // 0 - 全部,1 - 有效,2 - 过期
|
||||
}
|
||||
|
||||
func ListResourceLong(c *fiber.Ctx) error {
|
||||
// PageResourceLong 分页查询当前用户长效套餐
|
||||
func PageResourceLong(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
@@ -115,7 +121,7 @@ func ListResourceLong(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(ListResourceLongReq)
|
||||
req := new(PageResourceLongReq)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,16 +141,29 @@ func ListResourceLong(c *fiber.Ctx) error {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Type.Eq(int(*req.Type)))
|
||||
}
|
||||
if req.CreateAfter != nil {
|
||||
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter))
|
||||
do = do.Where(q.Resource.CreatedAt.Gte(req.CreateAfter.UTC()))
|
||||
}
|
||||
if req.CreateBefore != nil {
|
||||
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
||||
do = do.Where(q.Resource.CreatedAt.Lte(req.CreateBefore.UTC()))
|
||||
}
|
||||
if req.ExpireAfter != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Gte(*req.ExpireAfter))
|
||||
do = do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(req.ExpireAfter.UTC()))
|
||||
}
|
||||
if req.ExpireBefore != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Lte(*req.ExpireBefore))
|
||||
do = do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(req.ExpireBefore.UTC()))
|
||||
}
|
||||
if req.Status != nil {
|
||||
var long = q.ResourceLong.As(q.Resource.Long.Name())
|
||||
switch *req.Status {
|
||||
case 1:
|
||||
var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Gte(time.Now().UTC()))
|
||||
var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.GtCol(long.Used))
|
||||
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
|
||||
case 2:
|
||||
var timeCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeTime)), long.ExpireAt.Lte(time.Now().UTC()))
|
||||
var quotaCond = q.Resource.Where(long.Type.Eq(int(m.ResourceModeQuota)), long.Quota.LteCol(long.Used))
|
||||
do.Where(q.Resource.Where(timeCond).Or(quotaCond))
|
||||
}
|
||||
}
|
||||
|
||||
resource, err := q.Resource.Where(do).
|
||||
@@ -162,6 +181,7 @@ func ListResourceLong(c *fiber.Ctx) error {
|
||||
total = int64(len(resource) + req.GetOffset())
|
||||
} else {
|
||||
total, err = q.Resource.
|
||||
Joins(q.Resource.Long).
|
||||
Where(do).
|
||||
Count()
|
||||
if err != nil {
|
||||
@@ -177,9 +197,341 @@ 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"`
|
||||
Status *int `json:"status"` // 0 - 全部,1 - 有效,2 - 过期
|
||||
}
|
||||
|
||||
// PageResourceShortByAdmin 分页查询全部短效套餐
|
||||
func PageResourceShortByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeResourceShortRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageResourceShortByAdminReq
|
||||
if err = g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do := q.Resource.Where()
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.Active != nil {
|
||||
do = do.Where(q.Resource.Active.Is(*req.Active))
|
||||
}
|
||||
if req.Mode != nil {
|
||||
do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
if req.Expired != nil {
|
||||
if *req.Expired {
|
||||
do = do.Where(q.Resource.Where(
|
||||
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)),
|
||||
q.ResourceShort.As("Short").ExpireAt.Lte(time.Now().UTC()),
|
||||
).Or(
|
||||
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
|
||||
q.ResourceShort.As("Short").Quota.LteCol(q.ResourceShort.As("Short").Used),
|
||||
))
|
||||
} else {
|
||||
do = do.Where(q.Resource.Where(
|
||||
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeTime)),
|
||||
q.ResourceShort.As("Short").ExpireAt.Gt(time.Now().UTC()),
|
||||
).Or(
|
||||
q.ResourceShort.As("Short").Type.Eq(int(m.ResourceModeQuota)),
|
||||
q.ResourceShort.As("Short").Quota.GtCol(q.ResourceShort.As("Short").Used),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := q.Resource.
|
||||
Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku).
|
||||
Select(
|
||||
q.Resource.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.ResourceShort.As("Short").Type.As("Short__type"),
|
||||
q.ResourceShort.As("Short").Live.As("Short__live"),
|
||||
q.ResourceShort.As("Short").Quota.As("Short__quota"),
|
||||
q.ResourceShort.As("Short").Used.As("Short__used"),
|
||||
q.ResourceShort.As("Short").Daily.As("Short__daily"),
|
||||
q.ResourceShort.As("Short").LastAt.As("Short__last_at"),
|
||||
q.ResourceShort.As("Short").ExpireAt.As("Short__expire_at"),
|
||||
q.ProductSku.As("Short__Sku").Name.As("Short__Sku__name"),
|
||||
).
|
||||
Where(q.Resource.Type.Eq(int(m.ResourceTypeShort)), do).
|
||||
Order(q.Resource.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageResourceShortByAdminReq struct {
|
||||
core.PageReq
|
||||
UserPhone *string `json:"user_phone" form:"user_phone"`
|
||||
ResourceNo *string `json:"resource_no" form:"resource_no"`
|
||||
Active *bool `json:"active" form:"active"`
|
||||
Mode *int `json:"mode" form:"mode"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start" form:"created_at_start"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end" form:"created_at_end"`
|
||||
Expired *bool `json:"expired" form:"expired"`
|
||||
}
|
||||
|
||||
// PageResourceLongByAdmin 分页查询全部长效套餐
|
||||
func PageResourceLongByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeResourceLongRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageResourceLongByAdminReq
|
||||
if err = g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do := q.Resource.Where()
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.Active != nil {
|
||||
do = do.Where(q.Resource.Active.Is(*req.Active))
|
||||
}
|
||||
if req.Mode != nil {
|
||||
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
if req.Expired != nil {
|
||||
if *req.Expired {
|
||||
do = do.Where(q.Resource.Where(
|
||||
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)),
|
||||
q.ResourceLong.As("Long").ExpireAt.Lte(time.Now().UTC()),
|
||||
).Or(
|
||||
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
|
||||
q.ResourceLong.As("Long").Quota.LteCol(q.ResourceLong.As("Long").Used),
|
||||
))
|
||||
} else {
|
||||
do = do.Where(q.Resource.Where(
|
||||
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeTime)),
|
||||
q.ResourceLong.As("Long").ExpireAt.Gt(time.Now().UTC()),
|
||||
).Or(
|
||||
q.ResourceLong.As("Long").Type.Eq(int(m.ResourceModeQuota)),
|
||||
q.ResourceLong.As("Long").Quota.GtCol(q.ResourceLong.As("Long").Used),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := q.Resource.
|
||||
Joins(q.Resource.User, q.Resource.Long, q.Resource.Long.Sku).
|
||||
Select(
|
||||
q.Resource.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.ResourceLong.As("Long").Type.As("Long__type"),
|
||||
q.ResourceLong.As("Long").Live.As("Long__live"),
|
||||
q.ResourceLong.As("Long").Quota.As("Long__quota"),
|
||||
q.ResourceLong.As("Long").Used.As("Long__used"),
|
||||
q.ResourceLong.As("Long").Daily.As("Long__daily"),
|
||||
q.ResourceLong.As("Long").LastAt.As("Long__last_at"),
|
||||
q.ResourceLong.As("Long").ExpireAt.As("Long__expire_at"),
|
||||
q.ProductSku.As("Long__Sku").Name.As("Long__Sku__name"),
|
||||
).
|
||||
Where(q.Resource.Type.Eq(int(m.ResourceTypeLong)), do).
|
||||
Order(q.Resource.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageResourceLongByAdminReq struct {
|
||||
core.PageReq
|
||||
UserPhone *string `json:"user_phone" form:"user_phone"`
|
||||
ResourceNo *string `json:"resource_no" form:"resource_no"`
|
||||
Active *bool `json:"active" form:"active"`
|
||||
Mode *int `json:"mode" form:"mode"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start" form:"created_at_start"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end" form:"created_at_end"`
|
||||
Expired *bool `json:"expired" form:"expired"`
|
||||
}
|
||||
|
||||
// PageResourceShortOfUserByAdmin 分页查询指定用户的短效套餐
|
||||
func PageResourceShortOfUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeResourceShortReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageResourceShortOfUserByAdminReq
|
||||
if err = g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do := q.Resource.Where(q.Resource.UserID.Eq(req.UserID))
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.Active != nil {
|
||||
do = do.Where(q.Resource.Active.Is(*req.Active))
|
||||
}
|
||||
if req.Mode != nil {
|
||||
do = do.Where(q.ResourceShort.As("Short").Type.Eq(int(*req.Mode)))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
list, total, err := q.Resource.Debug().
|
||||
Joins(q.Resource.User, q.Resource.Short, q.Resource.Short.Sku).
|
||||
Select(
|
||||
q.Resource.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.ResourceShort.As("Short").Type.As("Short__type"),
|
||||
q.ResourceShort.As("Short").Live.As("Short__live"),
|
||||
q.ResourceShort.As("Short").Quota.As("Short__quota"),
|
||||
q.ResourceShort.As("Short").Used.As("Short__used"),
|
||||
q.ResourceShort.As("Short").Daily.As("Short__daily"),
|
||||
q.ResourceShort.As("Short").LastAt.As("Short__last_at"),
|
||||
q.ResourceShort.As("Short").ExpireAt.As("Short__expire_at"),
|
||||
q.ProductSku.As("Short__Sku").Name.As("Short__Sku__name"),
|
||||
).
|
||||
Where(q.Resource.Type.Eq(int(m.ResourceTypeShort)), do).
|
||||
Order(q.Resource.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageResourceShortOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
Active *bool `json:"active"`
|
||||
Mode *int `json:"mode"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end"`
|
||||
}
|
||||
|
||||
// PageResourceLongOfUserByAdmin 分页查询指定用户的长效套餐
|
||||
func PageResourceLongOfUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeResourceLongReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req PageResourceLongOfUserByAdminReq
|
||||
if err = g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do := q.Resource.Where(q.Resource.UserID.Eq(req.UserID))
|
||||
if req.ResourceNo != nil {
|
||||
do = do.Where(q.Resource.ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.Active != nil {
|
||||
do = do.Where(q.Resource.Active.Is(*req.Active))
|
||||
}
|
||||
if req.Mode != nil {
|
||||
do = do.Where(q.ResourceLong.As("Long").Type.Eq(*req.Mode))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Resource.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
list, total, err := q.Resource.
|
||||
Joins(q.Resource.User, q.Resource.Long, q.Resource.Long.Sku).
|
||||
Select(
|
||||
q.Resource.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
q.ResourceLong.As("Long").Type.As("Long__type"),
|
||||
q.ResourceLong.As("Long").Live.As("Long__live"),
|
||||
q.ResourceLong.As("Long").Quota.As("Long__quota"),
|
||||
q.ResourceLong.As("Long").Used.As("Long__used"),
|
||||
q.ResourceLong.As("Long").Daily.As("Long__daily"),
|
||||
q.ResourceLong.As("Long").LastAt.As("Long__last_at"),
|
||||
q.ResourceLong.As("Long").ExpireAt.As("Long__expire_at"),
|
||||
q.ProductSku.As("Long__Sku").Name.As("Long__Sku__name"),
|
||||
).
|
||||
Where(q.Resource.Type.Eq(int(m.ResourceTypeLong)), do).
|
||||
Order(q.Resource.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageResourceLongOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
Active *bool `json:"active"`
|
||||
Mode *int `json:"mode"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end"`
|
||||
}
|
||||
|
||||
// AllActiveResource 所有可用套餐
|
||||
func AllActiveResource(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -196,6 +548,8 @@ func AllActiveResource(c *fiber.Ctx) error {
|
||||
Joins(
|
||||
q.Resource.Short,
|
||||
q.Resource.Long,
|
||||
q.Resource.Short.Sku,
|
||||
q.Resource.Long.Sku,
|
||||
).
|
||||
Where(
|
||||
q.Resource.UserID.Eq(authCtx.User.ID),
|
||||
@@ -204,10 +558,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
||||
q.Resource.Type.Eq(int(m.ResourceTypeShort)),
|
||||
q.ResourceShort.As(q.Resource.Short.Name()).Where(
|
||||
short.Type.Eq(int(m.ResourceModeTime)),
|
||||
short.Expire.Gte(now),
|
||||
short.ExpireAt.Gte(now.UTC()),
|
||||
q.ResourceShort.As(q.Resource.Short.Name()).
|
||||
Where(short.DailyLast.Lt(u.Today())).
|
||||
Or(short.DailyLimit.GtCol(short.DailyUsed)),
|
||||
Where(short.LastAt.Lt(u.Today().UTC())).
|
||||
Or(short.Quota.GtCol(short.Daily)),
|
||||
).Or(
|
||||
short.Type.Eq(int(m.ResourceModeQuota)),
|
||||
short.Quota.GtCol(short.Used),
|
||||
@@ -216,10 +570,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
||||
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
|
||||
q.ResourceLong.As(q.Resource.Long.Name()).Where(
|
||||
long.Type.Eq(int(m.ResourceModeTime)),
|
||||
long.Expire.Gte(now),
|
||||
long.ExpireAt.Gte(now.UTC()),
|
||||
q.ResourceLong.As(q.Resource.Long.Name()).
|
||||
Where(long.DailyLast.Lt(u.Today())).
|
||||
Or(long.DailyLimit.GtCol(long.DailyUsed)),
|
||||
Where(long.LastAt.Lt(u.Today().UTC())).
|
||||
Or(long.Quota.GtCol(long.Daily)),
|
||||
).Or(
|
||||
long.Type.Eq(int(m.ResourceModeQuota)),
|
||||
long.Quota.GtCol(long.Used),
|
||||
@@ -232,26 +586,61 @@ func AllActiveResource(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
switch resource.Type {
|
||||
case m.ResourceTypeShort:
|
||||
resource.Short.Sku = &m.ProductSku{Name: resource.Short.Sku.Name}
|
||||
case m.ResourceTypeLong:
|
||||
resource.Long.Sku = &m.ProductSku{Name: resource.Long.Sku.Name}
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(resources)
|
||||
}
|
||||
|
||||
type StatisticPersonalResp struct {
|
||||
Short StatisticShort `json:"short"`
|
||||
Long StatisticLong `json:"long"`
|
||||
func UpdateResourceByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeResourceWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateResourceData
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Resource.Update(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
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"`
|
||||
func UpdateResourceCheckIP(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req struct {
|
||||
core.IdReq
|
||||
CheckIP bool `json:"checkip"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Resource.Update(&s.UpdateResourceData{
|
||||
IdReq: req.IdReq,
|
||||
CheckIP: &req.CheckIP,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
// StatisticResourceFree 统计每日可用
|
||||
func StatisticResourceFree(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -282,39 +671,39 @@ func StatisticResourceFree(c *fiber.Ctx) error {
|
||||
|
||||
// 短效包量
|
||||
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeQuota:
|
||||
if u.Z(resource.Short.Quota) > resource.Short.Used {
|
||||
if resource.Short.Quota > resource.Short.Used {
|
||||
shortCount++
|
||||
shortQuotaSum += int(u.Z(resource.Short.Quota) - resource.Short.Used)
|
||||
shortQuotaSum += int(resource.Short.Quota - resource.Short.Used)
|
||||
}
|
||||
|
||||
// 长效包量
|
||||
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeQuota:
|
||||
if u.Z(resource.Long.Quota) > resource.Long.Used {
|
||||
if resource.Long.Quota > resource.Long.Used {
|
||||
longCount++
|
||||
longQuotaSum += int(u.Z(resource.Long.Quota) - resource.Long.Used)
|
||||
longQuotaSum += int(resource.Long.Quota - resource.Long.Used)
|
||||
}
|
||||
|
||||
// 短效包时
|
||||
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeTime:
|
||||
if time.Time(*resource.Short.Expire).After(time.Now()) {
|
||||
if resource.Short.DailyLast == nil || u.IsToday(time.Time(*resource.Short.DailyLast)) == false {
|
||||
if time.Time(*resource.Short.ExpireAt).After(time.Now()) {
|
||||
if resource.Short.LastAt == nil || u.IsToday(time.Time(*resource.Short.LastAt)) == false {
|
||||
shortCount++
|
||||
shortDailyFreeSum += int(resource.Short.DailyLimit)
|
||||
} else if resource.Short.DailyLimit > resource.Short.DailyUsed {
|
||||
shortDailyFreeSum += int(resource.Short.Quota)
|
||||
} else if resource.Short.Quota > resource.Short.Daily {
|
||||
shortCount++
|
||||
shortDailyFreeSum += int(resource.Short.DailyLimit - resource.Short.DailyUsed)
|
||||
shortDailyFreeSum += int(resource.Short.Quota - resource.Short.Daily)
|
||||
}
|
||||
}
|
||||
|
||||
// 长效包时
|
||||
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeTime:
|
||||
if time.Time(*resource.Long.Expire).After(time.Now()) {
|
||||
if resource.Long.DailyLast == nil || u.IsToday(time.Time(*resource.Long.DailyLast)) == false {
|
||||
if time.Time(*resource.Long.ExpireAt).After(time.Now()) {
|
||||
if resource.Long.LastAt == nil || u.IsToday(time.Time(*resource.Long.LastAt)) == false {
|
||||
longCount++
|
||||
longDailyFreeSum += int(resource.Long.DailyLimit)
|
||||
} else if resource.Long.DailyLimit > resource.Long.DailyUsed {
|
||||
longDailyFreeSum += int(resource.Long.Quota)
|
||||
} else if resource.Long.Quota > resource.Long.Daily {
|
||||
longCount++
|
||||
longDailyFreeSum += int(resource.Long.DailyLimit - resource.Long.DailyUsed)
|
||||
longDailyFreeSum += int(resource.Long.Quota - resource.Long.Daily)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,17 +723,24 @@ func StatisticResourceFree(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
type StatisticResourceUsageReq struct {
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
TimeAfter *time.Time `json:"time_after"`
|
||||
TimeBefore *time.Time `json:"time_before"`
|
||||
type StatisticPersonalResp struct {
|
||||
Short StatisticShort `json:"short"`
|
||||
Long StatisticLong `json:"long"`
|
||||
}
|
||||
|
||||
type StatisticResourceUsageResp []struct {
|
||||
Date time.Time `json:"date"`
|
||||
Count int `json:"count"`
|
||||
type StatisticShort struct {
|
||||
ResourceCount int `json:"resource_count"`
|
||||
ResourceQuotaSum int `json:"resource_quota_sum"`
|
||||
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
|
||||
}
|
||||
|
||||
type StatisticLong struct {
|
||||
ResourceCount int `json:"resource_count"`
|
||||
ResourceQuotaSum int `json:"resource_quota_sum"`
|
||||
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
|
||||
}
|
||||
|
||||
// StatisticResourceUsage 统计每日用量
|
||||
func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -359,26 +755,15 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 统计套餐提取数量
|
||||
do := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID))
|
||||
if req.ResourceNo != nil && *req.ResourceNo != "" {
|
||||
var resourceID int32
|
||||
err := q.Resource.
|
||||
Where(
|
||||
q.Resource.UserID.Eq(authCtx.User.ID),
|
||||
q.Resource.ResourceNo.Eq(*req.ResourceNo),
|
||||
).
|
||||
Select(q.Resource.ID).
|
||||
Scan(&resourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
do.Where(q.LogsUserUsage.ResourceID.Eq(resourceID))
|
||||
}
|
||||
do := q.LogsUserUsage.Where(
|
||||
q.LogsUserUsage.UserID.Eq(authCtx.User.ID),
|
||||
)
|
||||
|
||||
if req.TimeAfter != nil {
|
||||
do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter))
|
||||
do = do.Where(q.LogsUserUsage.Time.Gte(req.TimeAfter.UTC()))
|
||||
}
|
||||
if req.TimeBefore != nil {
|
||||
do.Where(q.LogsUserUsage.Time.Lte(*req.TimeBefore))
|
||||
do = do.Where(q.LogsUserUsage.Time.Lte(req.TimeBefore.UTC()))
|
||||
}
|
||||
|
||||
var data = new(StatisticResourceUsageResp)
|
||||
@@ -389,7 +774,7 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
).
|
||||
Where(do).
|
||||
Group(
|
||||
field.NewUnsafeFieldRaw("date_trunc('day', time)"),
|
||||
field.NewField("", "date"),
|
||||
).
|
||||
Order(
|
||||
field.NewField("", "date").Desc(),
|
||||
@@ -402,10 +787,17 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
type CreateResourceReq struct {
|
||||
*s.CreateResourceData
|
||||
type StatisticResourceUsageReq struct {
|
||||
TimeAfter *time.Time `json:"time_start"`
|
||||
TimeBefore *time.Time `json:"time_end"`
|
||||
}
|
||||
|
||||
type StatisticResourceUsageResp []struct {
|
||||
Date time.Time `json:"date"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// CreateResource 创建套餐
|
||||
func CreateResource(c *fiber.Ctx) error {
|
||||
|
||||
// 检查权限
|
||||
@@ -421,7 +813,7 @@ func CreateResource(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 创建套餐
|
||||
err = s.Resource.CreateResourceByBalance(authCtx.User.ID, time.Now(), req.CreateResourceData)
|
||||
err = s.Resource.CreateResourceByBalance(authCtx.User, req.CreateResourceData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -429,21 +821,37 @@ func CreateResource(c *fiber.Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateResourceReq struct {
|
||||
*s.CreateResourceData
|
||||
}
|
||||
|
||||
// ResourcePrice 套餐价格
|
||||
func ResourcePrice(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitSecretClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ac := auth.GetAuthCtx(c)
|
||||
|
||||
// 解析请求参数
|
||||
var req = new(CreateResourceReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
return core.NewBizErr("接口参数解析异常", err)
|
||||
}
|
||||
|
||||
// 获取套餐价格
|
||||
return c.JSON(fiber.Map{
|
||||
"price": req.GetAmount().StringFixed(2),
|
||||
detail, err := req.TradeDetail(ac.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算折扣
|
||||
return c.JSON(ResourcePriceResp{
|
||||
Price: detail.Amount.StringFixed(2),
|
||||
Discounted: detail.Discounted.StringFixed(2),
|
||||
Actual: detail.Actual.StringFixed(2),
|
||||
})
|
||||
}
|
||||
|
||||
type ResourcePriceResp struct {
|
||||
Price string `json:"price"`
|
||||
Discounted string `json:"discounted"`
|
||||
Actual string `json:"actual"`
|
||||
}
|
||||
|
||||
@@ -9,84 +9,234 @@ import (
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
s "platform/web/services"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type TradeCreateReq struct {
|
||||
s.CreateTradeData
|
||||
Type m.TradeType `json:"type" validate:"required"`
|
||||
Resource *s.CreateResourceData `json:"resource,omitempty"`
|
||||
Recharge *s.RechargeProductInfo `json:"recharge,omitempty"`
|
||||
// PageTradeByAdmin 分页查询所有订单
|
||||
func PageTradeByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageTradeByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建查询语句
|
||||
do := q.Trade.Where()
|
||||
if req.UserPhone != nil {
|
||||
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
|
||||
}
|
||||
if req.InnerNo != nil {
|
||||
do = do.Where(q.Trade.InnerNo.Eq(*req.InnerNo))
|
||||
}
|
||||
if req.OuterNo != nil {
|
||||
do = do.Where(q.Trade.OuterNo.Eq(*req.OuterNo))
|
||||
}
|
||||
if req.Method != nil {
|
||||
do = do.Where(q.Trade.Method.Eq(*req.Method))
|
||||
}
|
||||
if req.Platform != nil {
|
||||
do = do.Where(q.Trade.Platform.Eq(*req.Platform))
|
||||
}
|
||||
if req.Status != nil {
|
||||
do = do.Where(q.Trade.Status.Eq(*req.Status))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Trade.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Trade.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
// 查询用户列表
|
||||
list, total, err := q.Trade.
|
||||
Joins(q.Trade.User).
|
||||
Select(
|
||||
q.Trade.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.Trade.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type TradeCreateResp struct {
|
||||
PayUrl string `json:"pay_url"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
type PageTradeByAdminReq struct {
|
||||
core.PageReq
|
||||
UserPhone *string `json:"user_phone,omitempty"`
|
||||
InnerNo *string `json:"inner_no,omitempty"`
|
||||
OuterNo *string `json:"outer_no,omitempty"`
|
||||
Method *int `json:"method,omitempty"`
|
||||
Platform *int `json:"platform,omitempty"`
|
||||
Status *int `json:"status,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
|
||||
// PageTradeOfUserByAdmin 分页查询指定用户的订单
|
||||
func PageTradeOfUserByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeReadOfUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageTradeOfUserByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建查询语句
|
||||
do := q.Trade.Where(q.Trade.UserID.Eq(req.UserID))
|
||||
if req.InnerNo != nil {
|
||||
do = do.Where(q.Trade.InnerNo.Eq(*req.InnerNo))
|
||||
}
|
||||
if req.OuterNo != nil {
|
||||
do = do.Where(q.Trade.OuterNo.Eq(*req.OuterNo))
|
||||
}
|
||||
if req.Method != nil {
|
||||
do = do.Where(q.Trade.Method.Eq(*req.Method))
|
||||
}
|
||||
if req.Platform != nil {
|
||||
do = do.Where(q.Trade.Platform.Eq(*req.Platform))
|
||||
}
|
||||
if req.Status != nil {
|
||||
do = do.Where(q.Trade.Status.Eq(*req.Status))
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.Trade.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.Trade.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
// 查询订单列表
|
||||
list, total, err := q.Trade.
|
||||
Joins(q.Trade.User).
|
||||
Select(
|
||||
q.Trade.ALL,
|
||||
q.User.As("User").Phone.As("User__phone"),
|
||||
q.User.As("User").Name.As("User__name"),
|
||||
).
|
||||
Where(do).
|
||||
Order(q.Trade.CreatedAt.Desc()).
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
type PageTradeOfUserByAdminReq struct {
|
||||
core.PageReq
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
InnerNo *string `json:"inner_no,omitempty"`
|
||||
OuterNo *string `json:"outer_no,omitempty"`
|
||||
Method *int `json:"method,omitempty"`
|
||||
Platform *int `json:"platform,omitempty"`
|
||||
Status *int `json:"status,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
// 创建订单
|
||||
func TradeCreate(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if authCtx.User.IDType == m.UserIDTypeUnverified {
|
||||
return core.NewBizErr("请先实名认证后再购买")
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(TradeCreateReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch req.Type {
|
||||
case m.TradeTypePurchase:
|
||||
if req.Resource == nil {
|
||||
return core.NewBizErr("购买信息不能为空")
|
||||
}
|
||||
req.Product = req.Resource
|
||||
case m.TradeTypeRecharge:
|
||||
if req.Recharge == nil {
|
||||
return core.NewBizErr("充值信息不能为空")
|
||||
}
|
||||
req.Product = req.Recharge
|
||||
}
|
||||
|
||||
// 创建交易
|
||||
result, err := s.Trade.CreateTrade(authCtx.User.ID, time.Now(), &req.CreateTradeData)
|
||||
// 处理订单
|
||||
var result *s.CreateTradeResult
|
||||
switch req.Type {
|
||||
case m.TradeTypePurchase:
|
||||
result, err = s.Trade.Create(authCtx.User, req.CreateTradeData, req.Resource)
|
||||
case m.TradeTypeRecharge:
|
||||
result, err = s.Trade.Create(authCtx.User, req.CreateTradeData, req.Recharge)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("创建交易失败", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "创建交易失败"})
|
||||
return core.NewServErr("处理购买产品信息失败", err)
|
||||
}
|
||||
|
||||
return c.JSON(&TradeCreateResp{
|
||||
PayUrl: result.PaymentUrl,
|
||||
TradeNo: result.TradeNo,
|
||||
})
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
type TradeCompleteReq struct {
|
||||
s.ModifyTradeData
|
||||
type TradeCreateReq struct {
|
||||
*s.CreateTradeData
|
||||
Type m.TradeType `json:"type" validate:"required"`
|
||||
Resource *s.CreateResourceData `json:"resource,omitempty"`
|
||||
Recharge *s.UpdateBalanceData `json:"recharge,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
// 完成订单
|
||||
func TradeComplete(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitUser()
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(TradeCompleteReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
var req s.TradeRef
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
err = s.Trade.CompleteTrade(&req.ModifyTradeData)
|
||||
err = s.Trade.CompleteTrade(authCtx.User, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -94,10 +244,41 @@ func TradeComplete(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
type TradeCancelReq struct {
|
||||
s.ModifyTradeData
|
||||
// 管理员完成订单
|
||||
func TradeCompleteByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeWriteComplete)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
var req struct {
|
||||
s.TradeRef
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
}
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := s.User.Get(q.Q, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 完成订单
|
||||
err = s.Trade.CompleteTrade(user, &req.TradeRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
// 取消订单
|
||||
func TradeCancel(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -112,7 +293,7 @@ func TradeCancel(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 取消交易
|
||||
err = s.Trade.CancelTrade(&req.ModifyTradeData, time.Now())
|
||||
err = s.Trade.CancelTrade(&req.TradeRef)
|
||||
if err != nil {
|
||||
slog.Error("取消交易失败", "trade_no", req.TradeNo, "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "取消交易失败"})
|
||||
@@ -121,11 +302,16 @@ func TradeCancel(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
type TradeCheckReq struct {
|
||||
s.ModifyTradeData
|
||||
type TradeCancelReq struct {
|
||||
s.TradeRef
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
// 检查订单
|
||||
func TradeCheck(c *fiber.Ctx) error {
|
||||
// 检查权限:sse 接口暂时不检查权限
|
||||
|
||||
// 解析请求参数
|
||||
req := new(TradeCheckReq)
|
||||
if err := g.Validator.ParseQuery(c, req); err != nil {
|
||||
@@ -142,7 +328,7 @@ func TradeCheck(c *fiber.Ctx) error {
|
||||
interval := 5
|
||||
for range expire / interval {
|
||||
// 检查订单状态
|
||||
result, err := s.Trade.CheckTrade(&req.ModifyTradeData)
|
||||
result, err := s.Trade.CheckTrade(&req.TradeRef)
|
||||
if err != nil {
|
||||
slog.Error("检查订单状态失败", "trade_no", req.TradeNo, "error", err)
|
||||
return
|
||||
@@ -154,9 +340,9 @@ func TradeCheck(c *fiber.Ctx) error {
|
||||
slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = w.Flush()
|
||||
if err != nil {
|
||||
slog.Error("刷新缓冲区失败", "trade_no", req.TradeNo, "error", err, "errType", reflect.TypeOf(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -171,3 +357,7 @@ func TradeCheck(c *fiber.Ctx) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TradeCheckReq struct {
|
||||
s.TradeRef
|
||||
}
|
||||
|
||||
@@ -1,24 +1,306 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
s "platform/web/services"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/shopspring/decimal"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gen/field"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// region /update
|
||||
// 分页获取用户
|
||||
func PageUserByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type UpdateUserReq struct {
|
||||
Username string `json:"username" validate:"omitempty,min=3,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"`
|
||||
// 解析请求参数
|
||||
req := new(PageUserByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
do := q.User.Where()
|
||||
if req.Account != nil {
|
||||
do = do.Where(q.User.Where(
|
||||
q.User.Username.Like("%" + *req.Account + "%"),
|
||||
).Or(
|
||||
q.User.Phone.Like("%" + *req.Account + "%"),
|
||||
).Or(
|
||||
q.User.Email.Like("%" + *req.Account + "%"),
|
||||
))
|
||||
}
|
||||
if req.Name != nil {
|
||||
do = do.Where(q.User.Name.Eq(*req.Name))
|
||||
}
|
||||
if req.Identified != nil {
|
||||
if *req.Identified {
|
||||
do = do.Where(q.User.IDType.Gt(0))
|
||||
} else {
|
||||
do = do.Where(q.User.IDType.Eq(0))
|
||||
}
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
if *req.Enabled {
|
||||
do = do.Where(q.User.Status.Eq(1))
|
||||
} else {
|
||||
do = do.Where(q.User.Status.Eq(0))
|
||||
}
|
||||
}
|
||||
if req.Assigned != nil {
|
||||
if *req.Assigned {
|
||||
do = do.Where(q.User.AdminID.IsNotNull())
|
||||
} else {
|
||||
do = do.Where(q.User.AdminID.IsNull())
|
||||
}
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
do = do.Where(q.User.CreatedAt.Gte(req.CreatedAtStart.UTC()))
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
do = do.Where(q.User.CreatedAt.Lte(req.CreatedAtEnd.UTC()))
|
||||
}
|
||||
|
||||
// 查询用户列表
|
||||
users, total, err := q.User.
|
||||
Preload(q.User.Admin, q.User.Discount).
|
||||
Omit(q.User.Password, q.Admin.Password).
|
||||
Where(do).
|
||||
Order(q.User.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
|
||||
if user.IDNo != nil && len(*user.IDNo) == 18 {
|
||||
var str = *user.IDNo
|
||||
*user.IDNo = str[:6] + "****" + str[len(str)-2:]
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
type PageUserByAdminReq struct {
|
||||
core.PageReq
|
||||
Account *string `json:"account,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Identified *bool `json:"identified,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Assigned *bool `json:"assigned,omitempty"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
|
||||
}
|
||||
|
||||
// 管理员获取单个用户
|
||||
func GetUserByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserReadOne)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
var req GetUserByAdminReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
do := q.User.Where()
|
||||
if req.Account != nil {
|
||||
do = do.Where(q.User.Where(
|
||||
q.User.Username.Like("%" + *req.Account + "%"),
|
||||
).Or(
|
||||
q.User.Phone.Like("%" + *req.Account + "%"),
|
||||
).Or(
|
||||
q.User.Email.Like("%" + *req.Account + "%"),
|
||||
))
|
||||
}
|
||||
if req.Name != nil {
|
||||
do = do.Where(q.User.Name.Eq(*req.Name))
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
user, err := q.User.
|
||||
Preload(q.User.Admin, q.User.Discount).
|
||||
Omit(q.User.Password, q.Admin.Password).
|
||||
Where(do).
|
||||
Order(q.User.CreatedAt.Desc()).
|
||||
First()
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return core.NewBizErr("找不到用户")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 仅保留管理员名称
|
||||
if user.Admin != nil {
|
||||
user.Admin = &m.Admin{
|
||||
Name: user.Admin.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
type GetUserByAdminReq struct {
|
||||
Account *string `json:"account,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// 管理员创建用户
|
||||
func CreateUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.CreateUserByAdminData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.User.CreateByAdmin(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
// 管理员更新用户
|
||||
func UpdateUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req s.UpdateUserByAdminData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.User.UpdateByAdmin(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
// 管理员删除用户
|
||||
func RemoveUserByAdmin(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req core.IdReq
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.User.RemoveByAdmin(req.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
// 管理员更新用户余额
|
||||
func UpdateUserBalanceByAdmin(c *fiber.Ctx) error {
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWriteBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req UpdateUserBalanceByAdminData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := s.User.Get(q.Q, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
balance, err := decimal.NewFromString(req.Balance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.User.UpdateBalanceByAdmin(user, balance, &authCtx.Admin.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
type UpdateUserBalanceByAdminData struct {
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
Balance string `json:"balance" validate:"required"`
|
||||
}
|
||||
|
||||
// 绑定管理员
|
||||
func BindAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWriteBind)
|
||||
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
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
r, 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 r.RowsAffected == 0 {
|
||||
return core.NewBizErr("用户已绑定管理员")
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
func UpdateUser(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -33,31 +315,48 @@ func UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
_, err = q.User.
|
||||
do := make([]field.AssignExpr, 0)
|
||||
if req.Username != nil && *req.Username != "" {
|
||||
do = append(do, q.User.Username.Value(*req.Username))
|
||||
}
|
||||
if req.Email != nil {
|
||||
if *req.Email == "" {
|
||||
do = append(do, q.User.Email.Null())
|
||||
} else {
|
||||
do = append(do, q.User.Email.Value(*req.Email))
|
||||
}
|
||||
}
|
||||
if req.ContactQQ != nil {
|
||||
do = append(do, q.User.ContactQQ.Value(*req.ContactQQ))
|
||||
}
|
||||
if req.ContactWechat != nil {
|
||||
do = append(do, q.User.ContactWechat.Value(*req.ContactWechat))
|
||||
}
|
||||
r, err := q.User.
|
||||
Where(q.User.ID.Eq(authCtx.User.ID)).
|
||||
Updates(m.User{
|
||||
Username: &req.Username,
|
||||
Email: &req.Email,
|
||||
ContactQQ: &req.ContactQQ,
|
||||
ContactWechat: &req.ContactWechat,
|
||||
})
|
||||
UpdateSimple(do...)
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return core.NewBizErr("用户名或邮箱已被占用")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return core.NewBizErr("用户状态已过期")
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region /update/account
|
||||
|
||||
type UpdateAccountReq struct {
|
||||
Username string `json:"username" validate:"omitempty,min=3,max=20"`
|
||||
Password string `json:"password" validate:"omitempty,min=6,max=20"`
|
||||
type UpdateUserReq struct {
|
||||
Username *string `json:"username" validate:"omitempty,min=3,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 {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -72,7 +371,7 @@ func UpdateAccount(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
_, err = q.User.
|
||||
r, err := q.User.
|
||||
Where(q.User.ID.Eq(authCtx.User.ID)).
|
||||
Updates(m.User{
|
||||
Username: &req.Username,
|
||||
@@ -81,21 +380,20 @@ func UpdateAccount(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return core.NewBizErr("用户状态已过期")
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region /update/password
|
||||
|
||||
type UpdatePasswordReq struct {
|
||||
Phone string `json:"phone"`
|
||||
Code string `json:"code"`
|
||||
Password string `json:"password"`
|
||||
type UpdateAccountReq struct {
|
||||
Username string `json:"username" validate:"omitempty,min=3,max=20"`
|
||||
Password string `json:"password" validate:"omitempty,min=6,max=20"`
|
||||
}
|
||||
|
||||
// 更新账号密码
|
||||
func UpdatePassword(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -110,10 +408,13 @@ func UpdatePassword(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 验证手机令牌
|
||||
if req.Phone == "" || req.Code == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空")
|
||||
if req.Code == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "验证码不能为空")
|
||||
}
|
||||
err = s.Verifier.VerifySms(c.Context(), authCtx.User.Phone, req.Code, s.VerifierSmsPurposePassword)
|
||||
if errors.Is(err, s.ErrVerifierServiceInvalid) {
|
||||
return core.NewBizErr(s.ErrVerifierServiceInvalid.Error())
|
||||
}
|
||||
err = s.Verifier.VerifySms(c.Context(), req.Phone, req.Code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,15 +425,136 @@ func UpdatePassword(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = q.User.
|
||||
r, err := q.User.
|
||||
Where(q.User.ID.Eq(authCtx.User.ID)).
|
||||
UpdateColumn(q.User.Password, newHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return core.NewBizErr("用户状态已过期")
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// endregion
|
||||
type UpdatePasswordReq struct {
|
||||
Code string `json:"code"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// PageUserNotBindByAdmin 分页获取未绑定管理员的用户
|
||||
func PageUserNotBindByAdmin(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserReadNotBind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(PageUserNotBindByAdminReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建查询条件(强制过滤未绑定管理员的用户)
|
||||
do := q.User.Where(q.User.AdminID.IsNull())
|
||||
if req.Phone != nil {
|
||||
do = do.Where(q.User.Phone.Eq(*req.Phone))
|
||||
}
|
||||
|
||||
// 查询用户列表
|
||||
users, total, err := q.User.
|
||||
Omit(q.User.Password, q.User.IDNo).
|
||||
Where(do).
|
||||
Order(q.User.CreatedAt.Desc()).
|
||||
FindByPage(req.GetOffset(), req.GetLimit())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: users,
|
||||
})
|
||||
}
|
||||
|
||||
type PageUserNotBindByAdminReq struct {
|
||||
core.PageReq
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateUserBalanceIncByAdmin 管理员增加用户余额
|
||||
func UpdateUserBalanceIncByAdmin(c *fiber.Ctx) error {
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWriteBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req UpdateUserBalanceChangeByAdminData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amount, err := decimal.NewFromString(req.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !amount.IsPositive() {
|
||||
return core.NewBizErr("金额必须为正数")
|
||||
}
|
||||
|
||||
user, err := s.User.Get(q.Q, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newBalance := user.Balance.Add(amount)
|
||||
if err := s.User.UpdateBalanceByAdmin(user, newBalance, &authCtx.Admin.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
// UpdateUserBalanceDecByAdmin 管理员减少用户余额
|
||||
func UpdateUserBalanceDecByAdmin(c *fiber.Ctx) error {
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeUserWriteBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req UpdateUserBalanceChangeByAdminData
|
||||
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amount, err := decimal.NewFromString(req.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !amount.IsPositive() {
|
||||
return core.NewBizErr("金额必须为正数")
|
||||
}
|
||||
|
||||
user, err := s.User.Get(q.Q, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newBalance := user.Balance.Sub(amount)
|
||||
if err := s.User.UpdateBalanceByAdmin(user, newBalance, &authCtx.Admin.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(nil)
|
||||
}
|
||||
|
||||
type UpdateUserBalanceChangeByAdminData struct {
|
||||
UserID int32 `json:"user_id" validate:"required"`
|
||||
Amount string `json:"amount" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"platform/pkg/env"
|
||||
"platform/web/auth"
|
||||
"platform/web/services"
|
||||
s "platform/web/services"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
@@ -13,12 +14,11 @@ import (
|
||||
)
|
||||
|
||||
type VerifierReq struct {
|
||||
Purpose services.VerifierSmsPurpose `json:"purpose"`
|
||||
Phone string `json:"phone"`
|
||||
Purpose s.VerifierSmsPurpose `json:"purpose"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
func SmsCode(c *fiber.Ctx) error {
|
||||
|
||||
func SendSmsCode(c *fiber.Ctx) error {
|
||||
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -38,9 +38,9 @@ func SmsCode(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 发送身份验证码
|
||||
err = services.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
|
||||
err = s.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
|
||||
if err != nil {
|
||||
var sErr services.VerifierServiceSendLimitErr
|
||||
var sErr s.VerifierServiceSendLimitErr
|
||||
if errors.As(err, &sErr) {
|
||||
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
|
||||
}
|
||||
@@ -51,6 +51,23 @@ func SmsCode(c *fiber.Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendSmsCodeForPassword(c *fiber.Ctx) error {
|
||||
ac, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Verifier.SendSms(c.Context(), ac.User.Phone, s.VerifierSmsPurposePassword); err != nil {
|
||||
var sErr s.VerifierServiceSendLimitErr
|
||||
if errors.As(err, &sErr) {
|
||||
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DebugGetSmsCode(c *fiber.Ctx) error {
|
||||
if env.RunMode != env.RunModeDev {
|
||||
return fiber.NewError(fiber.StatusForbidden, "not allowed")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/auth"
|
||||
@@ -92,17 +94,35 @@ func CreateWhitelist(c *fiber.Ctx) error {
|
||||
|
||||
ip, err := secureAddr(req.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
return core.NewBizErr("IP 地址无效", err)
|
||||
}
|
||||
|
||||
// 创建白名单
|
||||
err = q.Whitelist.Create(&m.Whitelist{
|
||||
UserID: authCtx.User.ID,
|
||||
IP: u.Z(ip),
|
||||
Remark: &req.Remark,
|
||||
uid := authCtx.User.ID
|
||||
err = g.Redsync.WithLock(whitelistKey(uid), func() error {
|
||||
count, err := q.Whitelist.Where(
|
||||
q.Whitelist.UserID.Eq(uid),
|
||||
).Count()
|
||||
if err != nil {
|
||||
return core.NewServErr("获取白名单数量失败", err)
|
||||
}
|
||||
if count >= 5 {
|
||||
return core.NewBizErr("白名单数量已达上限")
|
||||
}
|
||||
|
||||
err = q.Whitelist.Create(&m.Whitelist{
|
||||
UserID: authCtx.User.ID,
|
||||
IP: u.Z(ip),
|
||||
Remark: &req.Remark,
|
||||
})
|
||||
if err != nil {
|
||||
return core.NewServErr("添加白名单失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return core.NewServErr("添加白名单失败", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -132,11 +152,11 @@ func UpdateWhitelist(c *fiber.Ctx) error {
|
||||
|
||||
ip, err := secureAddr(req.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
return core.NewBizErr("IP 地址无效", err)
|
||||
}
|
||||
|
||||
// 更新白名单
|
||||
_, err = q.Whitelist.
|
||||
r, err := q.Whitelist.
|
||||
Where(
|
||||
q.Whitelist.ID.Eq(req.ID),
|
||||
q.Whitelist.UserID.Eq(authCtx.User.ID),
|
||||
@@ -148,6 +168,9 @@ func UpdateWhitelist(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return core.NewBizErr("白名单状态已过期")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -181,7 +204,7 @@ func RemoveWhitelist(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 删除白名单
|
||||
_, err = q.Whitelist.
|
||||
r, err := q.Whitelist.
|
||||
Where(
|
||||
q.Whitelist.ID.In(ids...),
|
||||
q.Whitelist.UserID.Eq(authCtx.User.ID),
|
||||
@@ -192,6 +215,9 @@ func RemoveWhitelist(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RowsAffected == 0 {
|
||||
return core.NewBizErr("白名单状态已过期")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -201,7 +227,11 @@ func secureAddr(str string) (*orm.Inet, error) {
|
||||
return nil, err
|
||||
}
|
||||
if !ip.IsGlobalUnicast() && env.RunMode != env.RunModeDev {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "IP 地址不可用")
|
||||
return nil, errors.New("IP 地址不可用")
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func whitelistKey(userID int32) string {
|
||||
return fmt.Sprintf("platform:whitelist:add:%d", userID)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"platform/pkg/env"
|
||||
"platform/web/auth"
|
||||
|
||||
"github.com/gofiber/contrib/otelfiber/v2"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jxskiss/base62"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func ApplyMiddlewares(app *fiber.App) {
|
||||
@@ -20,6 +25,9 @@ func ApplyMiddlewares(app *fiber.App) {
|
||||
EnableStackTrace: true,
|
||||
}))
|
||||
|
||||
// metric
|
||||
app.Use(otelfiber.Middleware())
|
||||
|
||||
// logger
|
||||
app.Use(logger.New(logger.Config{
|
||||
Next: func(c *fiber.Ctx) bool {
|
||||
@@ -27,8 +35,31 @@ func ApplyMiddlewares(app *fiber.App) {
|
||||
},
|
||||
}))
|
||||
|
||||
// metric
|
||||
app.Use(otelfiber.Middleware())
|
||||
// 补充 otel span attr
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
err := c.Next()
|
||||
|
||||
span := trace.SpanFromContext(c.UserContext())
|
||||
if !span.IsRecording() {
|
||||
return err
|
||||
}
|
||||
|
||||
str := ""
|
||||
if err != nil {
|
||||
str = err.Error()
|
||||
}
|
||||
|
||||
span.SetAttributes(attribute.String("http.response.error", str))
|
||||
return err
|
||||
})
|
||||
|
||||
// cors
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowCredentials: true,
|
||||
AllowOriginsFunc: func(origin string) bool {
|
||||
return true
|
||||
},
|
||||
}))
|
||||
|
||||
// request id
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
@@ -38,8 +69,10 @@ func ApplyMiddlewares(app *fiber.App) {
|
||||
},
|
||||
}))
|
||||
|
||||
// cors
|
||||
app.Use(cors.New())
|
||||
// static uploads
|
||||
app.Use("/uploads", filesystem.New(filesystem.Config{
|
||||
Root: http.Dir(env.UploadDir),
|
||||
}))
|
||||
|
||||
// authenticate
|
||||
app.Use(auth.Authenticate())
|
||||
|
||||
@@ -10,16 +10,19 @@ import (
|
||||
// Admin 管理员表
|
||||
type Admin struct {
|
||||
core.Model
|
||||
Username string `json:"username" gorm:"column:username"` // 用户名
|
||||
Password string `json:"password" gorm:"column:password"` // 密码
|
||||
Name *string `json:"name" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL
|
||||
Phone *string `json:"phone" gorm:"column:phone"` // 手机号码
|
||||
Email *string `json:"email" gorm:"column:email"` // 邮箱
|
||||
Status AdminStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理
|
||||
Username string `json:"username" gorm:"column:username"` // 用户名
|
||||
Password string `json:"-" gorm:"column:password"` // 密码
|
||||
Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
|
||||
Phone *string `json:"phone,omitempty" gorm:"column:phone"` // 手机号码
|
||||
Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
|
||||
Status AdminStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
|
||||
Lock bool `json:"lock" gorm:"column:lock"` // 是否锁定编辑
|
||||
|
||||
Roles []*AdminRole `json:"roles" gorm:"many2many:link_admin_role"`
|
||||
}
|
||||
|
||||
// AdminStatus 管理员状态枚举
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
// AdminRole 管理员角色表
|
||||
type AdminRole struct {
|
||||
core.Model
|
||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||
Description *string `json:"description" gorm:"column:description"` // 角色描述
|
||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 角色描述
|
||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
|
||||
Permissions []*Permission `json:"permissions" gorm:"many2many:link_admin_role_permission"`
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
// Announcement 公告表
|
||||
type Announcement struct {
|
||||
core.Model
|
||||
Title string `json:"title" gorm:"column:title"` // 公告标题
|
||||
Content *string `json:"content" gorm:"column:content"` // 公告内容
|
||||
Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型:1-普通公告
|
||||
Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶
|
||||
Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态:0-禁用,1-正常
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 公告排序
|
||||
Title string `json:"title" gorm:"column:title"` // 公告标题
|
||||
Content *string `json:"content,omitempty" gorm:"column:content"` // 公告内容
|
||||
Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型:1-普通公告
|
||||
Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶
|
||||
Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态:0-禁用,1-正常
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 公告排序
|
||||
}
|
||||
|
||||
// AnnouncementType 公告类型枚举
|
||||
|
||||
20
web/models/area.go
Normal file
20
web/models/area.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
import "platform/web/core"
|
||||
|
||||
// Area 地区表
|
||||
type Area struct {
|
||||
core.Model
|
||||
Name string `json:"name" gorm:"column:name"` // 地区名称
|
||||
Level AreaLevel `json:"level" gorm:"column:level"` // 地区层级:1-省,2-市
|
||||
ParentID *int32 `json:"parent_id,omitempty" gorm:"column:parent_id"` // 父级地区ID
|
||||
Parent *Area `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
|
||||
}
|
||||
|
||||
// AreaLevel 地区层级枚举
|
||||
type AreaLevel int
|
||||
|
||||
const (
|
||||
AreaLevelProvince AreaLevel = 1 // 省
|
||||
AreaLevelCity AreaLevel = 2 // 市
|
||||
)
|
||||
23
web/models/article.go
Normal file
23
web/models/article.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import "platform/web/core"
|
||||
|
||||
// Article 文章表
|
||||
type Article struct {
|
||||
core.Model
|
||||
GroupID int32 `json:"group_id" gorm:"column:group_id"` // 分组ID
|
||||
Title string `json:"title" gorm:"column:title"` // 文章标题
|
||||
Content *string `json:"content,omitempty" gorm:"column:content"` // 文章内容
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 文章排序
|
||||
Status ArticleStatus `json:"status" gorm:"column:status"` // 文章状态:0-禁用,1-正常
|
||||
|
||||
Group *ArticleGroup `json:"group,omitempty" gorm:"foreignKey:GroupID"` // 分组
|
||||
}
|
||||
|
||||
// ArticleStatus 文章状态
|
||||
type ArticleStatus int
|
||||
|
||||
const (
|
||||
ArticleStatusDisabled ArticleStatus = 0 // 禁用
|
||||
ArticleStatusEnabled ArticleStatus = 1 // 正常
|
||||
)
|
||||
20
web/models/article_group.go
Normal file
20
web/models/article_group.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
import "platform/web/core"
|
||||
|
||||
// ArticleGroup 文章分组表
|
||||
type ArticleGroup struct {
|
||||
core.Model
|
||||
Name string `json:"name" gorm:"column:name"` // 分组名称
|
||||
Code string `json:"code" gorm:"column:code"` // 分组编码
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 分组排序
|
||||
Status ArticleGroupStatus `json:"status" gorm:"column:status"` // 分组状态:0-禁用,1-正常
|
||||
}
|
||||
|
||||
// ArticleGroupStatus 分组状态
|
||||
type ArticleGroupStatus int
|
||||
|
||||
const (
|
||||
ArticleGroupStatusDisabled ArticleGroupStatus = 0 // 禁用
|
||||
ArticleGroupStatusEnabled ArticleGroupStatus = 1 // 正常
|
||||
)
|
||||
22
web/models/balance_activity.go
Normal file
22
web/models/balance_activity.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BalanceActivity 余额变动记录表
|
||||
type BalanceActivity struct {
|
||||
ID int32 `json:"id" gorm:"column:id;primaryKey"` // 记录ID
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
BillID *int32 `json:"bill_id,omitempty" gorm:"column:bill_id"` // 账单ID
|
||||
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
|
||||
Amount string `json:"amount" gorm:"column:amount"` // 变动金额
|
||||
BalancePrev string `json:"balance_prev" gorm:"column:balance_prev"` // 变动前余额
|
||||
BalanceCurr string `json:"balance_curr" gorm:"column:balance_curr"` // 变动后余额
|
||||
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 备注
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
|
||||
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Bill *Bill `json:"bill,omitempty" gorm:"foreignKey:BillID"`
|
||||
Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
|
||||
}
|
||||
@@ -9,19 +9,22 @@ import (
|
||||
// Bill 账单表
|
||||
type Bill struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
TradeID *int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
||||
ResourceID *int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
RefundID *int32 `json:"refund_id" gorm:"column:refund_id"` // 退款ID
|
||||
BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号
|
||||
Info *string `json:"info" gorm:"column:info"` // 产品可读信息
|
||||
Type BillType `json:"type" gorm:"column:type"` // 账单类型:1-消费,2-退款,3-充值
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 账单金额
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
TradeID *int32 `json:"trade_id,omitempty" gorm:"column:trade_id"` // 订单ID
|
||||
ResourceID *int32 `json:"resource_id,omitempty" gorm:"column:resource_id"` // 套餐ID
|
||||
RefundID *int32 `json:"refund_id,omitempty" gorm:"column:refund_id"` // 退款ID
|
||||
CouponUserID *int32 `json:"coupon_user_id,omitempty" gorm:"column:coupon_user_id"` // 优惠券发放ID
|
||||
BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号
|
||||
Info *string `json:"info,omitempty" gorm:"column:info"` // 产品可读信息
|
||||
Type BillType `json:"type" gorm:"column:type"` // 账单类型:1-消费,2-退款,3-充值
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 应付金额
|
||||
Actual decimal.Decimal `json:"actual" gorm:"column:actual"` // 实付金额
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Trade *Trade `json:"trade" gorm:"foreignKey:TradeID"`
|
||||
Resource *Resource `json:"resource" gorm:"foreignKey:ResourceID"`
|
||||
Refund *Refund `json:"refund" gorm:"foreignKey:RefundID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Trade *Trade `json:"trade,omitempty" gorm:"foreignKey:TradeID"`
|
||||
Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
Refund *Refund `json:"refund,omitempty" gorm:"foreignKey:RefundID"`
|
||||
CouponUser *CouponUser `json:"coupon,omitempty" gorm:"foreignKey:CouponUserID"`
|
||||
}
|
||||
|
||||
// BillType 账单类型枚举
|
||||
|
||||
@@ -9,23 +9,25 @@ import (
|
||||
// Channel 通道表
|
||||
type Channel struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
ProxyID int32 `json:"proxy_id" gorm:"column:proxy_id"` // 代理ID
|
||||
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
||||
Port uint16 `json:"port" gorm:"column:port"` // 代理端口
|
||||
EdgeID *int32 `json:"edge_id" gorm:"column:edge_id"` // 节点ID(手动配置)
|
||||
FilterISP *EdgeISP `json:"filter_isp" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp
|
||||
FilterProv *string `json:"filter_prov" gorm:"column:filter_prov"` // 省份过滤(自动配置)
|
||||
FilterCity *string `json:"filter_city" gorm:"column:filter_city"` // 城市过滤(自动配置)
|
||||
IP *orm.Inet `json:"ip" gorm:"column:ip"` // 节点地址
|
||||
Whitelists *string `json:"whitelists" gorm:"column:whitelists"` // IP白名单,逗号分隔
|
||||
Username *string `json:"username" gorm:"column:username"` // 用户名
|
||||
Password *string `json:"password" gorm:"column:password"` // 密码
|
||||
ExpiredAt time.Time `json:"expired_at" gorm:"column:expired_at"` // 过期时间
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
||||
ProxyID int32 `json:"proxy_id" gorm:"column:proxy_id"` // 代理ID
|
||||
Host string `json:"host" gorm:"column:host"` // 代理主机
|
||||
Port uint16 `json:"port" gorm:"column:port"` // 代理端口
|
||||
EdgeID *int32 `json:"edge_id,omitempty" gorm:"column:edge_id"` // 节点ID(手动配置)
|
||||
EdgeRef *string `json:"edge_ref,omitempty" gorm:"column:edge_ref"` // 外部节点引用,用于索引没有ID的外部非受控节点
|
||||
FilterISP *EdgeISP `json:"filter_isp,omitempty" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp
|
||||
FilterProv *string `json:"filter_prov,omitempty" gorm:"column:filter_prov"` // 省份过滤(自动配置)
|
||||
FilterCity *string `json:"filter_city,omitempty" gorm:"column:filter_city"` // 城市过滤(自动配置)
|
||||
IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // 节点地址
|
||||
Whitelists *string `json:"whitelists,omitempty" gorm:"column:whitelists"` // IP白名单,逗号分隔
|
||||
Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
|
||||
Password *string `json:"password,omitempty" gorm:"column:password"` // 密码
|
||||
ExpiredAt time.Time `json:"expired_at" gorm:"column:expired_at"` // 过期时间
|
||||
|
||||
User User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Resource Resource `json:"resource" gorm:"foreignKey:ResourceID"`
|
||||
Proxy Proxy `json:"proxy" gorm:"foreignKey:ProxyID"`
|
||||
Edge *Edge `json:"edge" gorm:"foreignKey:EdgeID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
Proxy *Proxy `json:"proxy,omitempty" gorm:"foreignKey:ProxyID"`
|
||||
Edge *Edge `json:"edge,omitempty" gorm:"foreignKey:EdgeID"`
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ import (
|
||||
// Client 客户端表
|
||||
type Client struct {
|
||||
core.Model
|
||||
ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符
|
||||
ClientSecret string `json:"client_secret" gorm:"column:client_secret"` // OAuth2客户端密钥
|
||||
RedirectURI *string `json:"redirect_uri" gorm:"column:redirect_uri"` // OAuth2 重定向URI
|
||||
Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范:1-native,2-browser,3-web,4-api
|
||||
Name string `json:"name" gorm:"column:name"` // 名称
|
||||
Icon *string `json:"icon" gorm:"column:icon"` // 图标URL
|
||||
Status ClientStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
Type ClientType `json:"type" gorm:"column:type"` // 类型:0-普通,1-官方
|
||||
ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符
|
||||
ClientSecret string `json:"-" gorm:"column:client_secret"` // OAuth2客户端密钥
|
||||
RedirectURI *string `json:"redirect_uri,omitempty" gorm:"column:redirect_uri"` // OAuth2 重定向URI
|
||||
Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范:1-native,2-browser,3-web,4-api
|
||||
Name string `json:"name" gorm:"column:name"` // 名称
|
||||
Icon *string `json:"icon,omitempty" gorm:"column:icon"` // 图标URL
|
||||
Status ClientStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
Type ClientType `json:"type" gorm:"column:type"` // 类型:0-普通,1-官方
|
||||
|
||||
Permissions []*Permission `json:"permissions" gorm:"many2many:link_client_permission"`
|
||||
}
|
||||
|
||||
// ClientSpec 客户端安全规范枚举
|
||||
|
||||
@@ -10,20 +10,29 @@ import (
|
||||
// Coupon 优惠券表
|
||||
type Coupon struct {
|
||||
core.Model
|
||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
Code string `json:"code" gorm:"column:code"` // 优惠券代码
|
||||
Remark *string `json:"remark" gorm:"column:remark"` // 优惠券备注
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额
|
||||
MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额
|
||||
Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态:0-未使用,1-已使用,2-已过期
|
||||
ExpireAt *time.Time `json:"expire_at" gorm:"column:expire_at"` // 过期时间
|
||||
Name string `json:"name" gorm:"column:name"` // 优惠券名称
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额
|
||||
MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额
|
||||
Count int32 `json:"count" gorm:"column:count"` // 优惠券数量
|
||||
Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态:0-禁用,1-正常
|
||||
ExpireType CouponExpireType `json:"expire_type" gorm:"column:expire_type"` // 过期类型:0-不过期,1-固定日期,2-相对日期(从发放时间算起)
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间,固定日期必填
|
||||
ExpireIn *int `json:"expire_in,omitempty" gorm:"column:expire_in"` // 过期时长(天),相对日期必填
|
||||
}
|
||||
|
||||
// CouponStatus 优惠券状态枚举
|
||||
// CouponStatus 优惠券使用状态枚举
|
||||
type CouponStatus int
|
||||
|
||||
const (
|
||||
CouponStatusUnused CouponStatus = 0 // 未使用
|
||||
CouponStatusUsed CouponStatus = 1 // 已使用
|
||||
CouponStatusExpired CouponStatus = 2 // 已过期
|
||||
CouponStatusDisabled CouponStatus = 0 // 禁用
|
||||
CouponStatusEnabled CouponStatus = 1 // 正常
|
||||
)
|
||||
|
||||
// CouponExpireType 优惠券过期类型枚举
|
||||
type CouponExpireType int
|
||||
|
||||
const (
|
||||
CouponExpireTypeNever CouponExpireType = 0 // 不过期
|
||||
CouponExpireTypeFixed CouponExpireType = 1 // 固定日期
|
||||
CouponExpireTypeRelative CouponExpireType = 2 // 相对日期
|
||||
)
|
||||
|
||||
26
web/models/coupon_user.go
Normal file
26
web/models/coupon_user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// CouponUser 优惠券发放表
|
||||
type CouponUser struct {
|
||||
ID int32 `json:"id" gorm:"column:id;primaryKey"` // 记录ID
|
||||
CouponID int32 `json:"coupon_id" gorm:"column:coupon_id"` // 优惠券ID
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
Status CouponUserStatus `json:"status" gorm:"column:status"` // 使用状态:0-未使用,1-已使用,2-已禁用
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
|
||||
UsedAt *time.Time `json:"used_at,omitempty" gorm:"column:used_at"` // 使用时间
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
|
||||
|
||||
Coupon *Coupon `json:"coupon,omitempty" gorm:"foreignKey:CouponID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// CouponUserStatus 优惠券发放状态枚举
|
||||
type CouponUserStatus int
|
||||
|
||||
const (
|
||||
CouponUserStatusUnused CouponUserStatus = 0 // 未使用
|
||||
CouponUserStatusUsed CouponUserStatus = 1 // 已使用
|
||||
CouponUserStatusDisabled CouponUserStatus = 2 // 已禁用
|
||||
)
|
||||
@@ -8,16 +8,17 @@ import (
|
||||
// Edge 节点表
|
||||
type Edge struct {
|
||||
core.Model
|
||||
Type EdgeType `json:"type" gorm:"column:type"` // 节点类型:1-自建
|
||||
Version int32 `json:"version" gorm:"column:version"` // 节点版本
|
||||
Mac string `json:"mac" gorm:"column:mac"` // 节点 mac 地址
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址
|
||||
ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商:0-未知,1-电信,2-联通,3-移动
|
||||
Prov string `json:"prov" gorm:"column:prov"` // 省份
|
||||
City string `json:"city" gorm:"column:city"` // 城市
|
||||
Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态:0-离线,1-正常
|
||||
RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟
|
||||
Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率
|
||||
Type EdgeType `json:"type" gorm:"column:type"` // 节点类型:1-自建,2-GOST chain
|
||||
Version int32 `json:"version" gorm:"column:version"` // 节点版本
|
||||
Mac string `json:"mac" gorm:"column:mac"` // 节点 mac 地址或 GOST chain 名称
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 节点地址或 GOST chain addr 的 IP
|
||||
Port *uint16 `json:"port,omitempty" gorm:"column:port"` // GOST chain addr 的端口
|
||||
ISP EdgeISP `json:"isp" gorm:"column:isp"` // 运营商:0-未知,1-电信,2-联通,3-移动
|
||||
AreaID *int32 `json:"area_id,omitempty" gorm:"column:area_id"` // 城市地区ID
|
||||
Status EdgeStatus `json:"status" gorm:"column:status"` // 节点状态:0-离线,1-正常
|
||||
RTT int32 `json:"rtt" gorm:"column:rtt"` // 最近平均延迟
|
||||
Loss int32 `json:"loss" gorm:"column:loss"` // 最近丢包率
|
||||
Area *Area `json:"area,omitempty" gorm:"foreignKey:AreaID"` // 地区
|
||||
}
|
||||
|
||||
// EdgeType 节点类型枚举
|
||||
@@ -25,6 +26,7 @@ type EdgeType int
|
||||
|
||||
const (
|
||||
EdgeTypeSelfBuilt EdgeType = 1 // 自建
|
||||
EdgeTypeGostChain EdgeType = 2 // GOST chain
|
||||
)
|
||||
|
||||
// EdgeStatus 节点状态枚举
|
||||
@@ -39,6 +41,7 @@ const (
|
||||
type EdgeISP int
|
||||
|
||||
const (
|
||||
EdgeISPUnknown EdgeISP = 0 // 未知/任意
|
||||
EdgeISPTelecom EdgeISP = 1 // 电信
|
||||
EdgeISPUnicom EdgeISP = 2 // 联通
|
||||
EdgeISPMobile EdgeISP = 3 // 移动
|
||||
|
||||
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 // 已处理
|
||||
)
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
// LinkAdminRole 管理员角色关联表
|
||||
type LinkAdminRole struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // 关联ID
|
||||
AdminID int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:role_id"` // 角色ID
|
||||
ID int32 `json:"id" gorm:"column:id"` // 关联ID
|
||||
AdminID int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:admin_role_id"` // 角色ID
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ package models
|
||||
// LinkAdminRolePermission 管理员角色权限关联表
|
||||
type LinkAdminRolePermission struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // 关联ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:role_id"` // 角色ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:admin_role_id"` // 角色ID
|
||||
PermissionID int32 `json:"permission_id" gorm:"column:permission_id"` // 权限ID
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
// LinkUserRole 用户角色关联表
|
||||
type LinkUserRole struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // 关联ID
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:role_id"` // 角色ID
|
||||
ID int32 `json:"id" gorm:"column:id"` // 关联ID
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:user_role_id"` // 角色ID
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ package models
|
||||
// LinkUserRolePermission 用户角色权限关联表
|
||||
type LinkUserRolePermission struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // 关联ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:role_id"` // 角色ID
|
||||
RoleID int32 `json:"role_id" gorm:"column:user_role_id"` // 角色ID
|
||||
PermissionID int32 `json:"permission_id" gorm:"column:permission_id"` // 权限ID
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ type LogsLogin struct {
|
||||
GrantType GrantType `json:"grant_type" gorm:"column:grant_type"` // 授权类型
|
||||
PasswordType PasswordType `json:"password_type" gorm:"column:password_type"` // 密码模式子授权类型
|
||||
Success bool `json:"success" gorm:"column:success"` // 登录是否成功
|
||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||
Time time.Time `json:"time" gorm:"column:time"` // 登录时间
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// GrantType 授权类型枚举
|
||||
|
||||
@@ -7,18 +7,18 @@ import (
|
||||
|
||||
// LogsRequest 访问日志表
|
||||
type LogsRequest struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // 访问日志ID
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||
UA string `json:"ua" gorm:"column:ua"` // 用户代理
|
||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID
|
||||
Method string `json:"method" gorm:"column:method"` // 请求方法
|
||||
Path string `json:"path" gorm:"column:path"` // 请求路径
|
||||
Status int16 `json:"status" gorm:"column:status"` // 响应状态码
|
||||
Error *string `json:"error" gorm:"column:error"` // 错误信息
|
||||
Time time.Time `json:"time" gorm:"column:time"` // 请求时间
|
||||
Latency string `json:"latency" gorm:"column:latency"` // 请求延迟
|
||||
ID int32 `json:"id" gorm:"column:id"` // 访问日志ID
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||
UA string `json:"ua" gorm:"column:ua"` // 用户代理
|
||||
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||
ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
|
||||
Method string `json:"method" gorm:"column:method"` // 请求方法
|
||||
Path string `json:"path" gorm:"column:path"` // 请求路径
|
||||
Status int16 `json:"status" gorm:"column:status"` // 响应状态码
|
||||
Error *string `json:"error,omitempty" gorm:"column:error"` // 错误信息
|
||||
Time time.Time `json:"time" gorm:"column:time"` // 请求时间
|
||||
Latency string `json:"latency" gorm:"column:latency"` // 请求延迟
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Client *Client `json:"client" gorm:"foreignKey:ClientID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID"`
|
||||
}
|
||||
|
||||
@@ -12,9 +12,12 @@ type LogsUserUsage struct {
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
||||
Count int32 `json:"count" gorm:"column:count"` // 数量
|
||||
Prov *string `json:"prov" gorm:"column:prov"` // 省份
|
||||
City *string `json:"city" gorm:"column:city"` // 城市
|
||||
ISP *string `json:"isp" gorm:"column:isp"` // 运营商
|
||||
Prov *string `json:"prov,omitempty" gorm:"column:prov"` // 省份
|
||||
City *string `json:"city,omitempty" gorm:"column:city"` // 城市
|
||||
ISP *string `json:"isp,omitempty" gorm:"column:isp"` // 运营商
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
|
||||
Time time.Time `json:"time" gorm:"column:time"` // 提取时间
|
||||
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import "platform/web/core"
|
||||
// Permission 权限表
|
||||
type Permission struct {
|
||||
core.Model
|
||||
ParentID *int32 `json:"parent_id" gorm:"column:parent_id"` // 父权限ID
|
||||
Name string `json:"name" gorm:"column:name"` // 权限名称
|
||||
Description *string `json:"description" gorm:"column:description"` // 权限描述
|
||||
ParentID *int32 `json:"parent_id,omitempty" gorm:"column:parent_id"` // 父权限ID
|
||||
Name string `json:"name" gorm:"column:name"` // 权限名称
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 权限描述
|
||||
Sort int `json:"sort" gorm:"column:sort"` // 排序
|
||||
|
||||
Parent *Permission `json:"parent" gorm:"foreignKey:ParentID"`
|
||||
Children []*Permission `json:"children" gorm:"foreignKey:ParentID"`
|
||||
Parent *Permission `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
|
||||
Children []*Permission `json:"children,omitempty" gorm:"foreignKey:ParentID"`
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
// Product 产品表
|
||||
type Product struct {
|
||||
core.Model
|
||||
Code string `json:"code" gorm:"column:code"` // 产品代码
|
||||
Name string `json:"name" gorm:"column:name"` // 产品名称
|
||||
Description *string `json:"description" gorm:"column:description"` // 产品描述
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态:0-禁用,1-正常
|
||||
Code string `json:"code" gorm:"column:code"` // 产品代码
|
||||
Name string `json:"name" gorm:"column:name"` // 产品名称
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 产品描述
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态:0-禁用,1-正常
|
||||
|
||||
Skus []*ProductSku `json:"skus,omitempty" gorm:"foreignKey:ProductID"` // 产品包含的SKU列表
|
||||
}
|
||||
|
||||
// ProductStatus 产品状态枚举
|
||||
|
||||
19
web/models/product_discount.go
Normal file
19
web/models/product_discount.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"platform/web/core"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// ProductDiscount 产品折扣表
|
||||
type ProductDiscount struct {
|
||||
core.Model
|
||||
Name string `json:"name" gorm:"column:name"` // 产品名称
|
||||
Discount int32 `json:"discount" gorm:"column:discount"` // 产品折扣
|
||||
}
|
||||
|
||||
func (pd ProductDiscount) Rate() decimal.Decimal {
|
||||
return decimal.NewFromInt32(pd.Discount).
|
||||
Div(decimal.NewFromInt32(100))
|
||||
}
|
||||
32
web/models/product_sku.go
Normal file
32
web/models/product_sku.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"platform/web/core"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// ProductSku 产品SKU表
|
||||
type ProductSku struct {
|
||||
core.Model
|
||||
ProductID int32 `json:"product_id" gorm:"column:product_id"` // 产品ID
|
||||
DiscountId int32 `json:"discount_id" gorm:"column:discount_id"` // 折扣,0 - 1 的小数,表示 xx 折
|
||||
Code string `json:"code" gorm:"column:code"` // SSKU 代码:格式为 key=value,key=value,...,其中,key:value 是 SKU 的属性,多个属性用逗号分隔
|
||||
Name string `json:"name" gorm:"column:name"` // SKU 可读名称
|
||||
Price decimal.Decimal `json:"price" gorm:"column:price"` // 定价
|
||||
PriceMin decimal.Decimal `json:"price_min" gorm:"column:price_min"` // 最低价格
|
||||
Status SkuStatus `json:"status" gorm:"column:status"` // SKU 状态:0-禁用,1-正常
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
CountMin int32 `json:"count_min" gorm:"column:count_min"` // 最小购买数量
|
||||
|
||||
Product *Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
|
||||
Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountId"`
|
||||
}
|
||||
|
||||
// SkuStatus SKU 状态
|
||||
type SkuStatus int32
|
||||
|
||||
const (
|
||||
SkuStatusDisabled SkuStatus = 0 // 禁用
|
||||
SkuStatusEnabled SkuStatus = 1 // 正常
|
||||
)
|
||||
19
web/models/product_sku_user.go
Normal file
19
web/models/product_sku_user.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProductSkuUser 用户产品SKU表
|
||||
type ProductSkuUser struct {
|
||||
ID int32 `json:"id" gorm:"column:id;primaryKey"`
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ProductSkuID int32 `json:"product_sku_id" gorm:"column:product_sku_id"` // 产品SKU ID
|
||||
DiscountId int32 `json:"discount_id" gorm:"column:discount_id"` // 折扣ID
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
ProductSku *ProductSku `json:"product_sku,omitempty" gorm:"foreignKey:ProductSkuID"`
|
||||
Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountId"`
|
||||
}
|
||||
@@ -10,15 +10,17 @@ import (
|
||||
// Proxy 代理服务表
|
||||
type Proxy struct {
|
||||
core.Model
|
||||
Version int32 `json:"version" gorm:"column:version"` // 代理服务版本
|
||||
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
||||
Secret *string `json:"secret" gorm:"column:secret"` // 代理服务密钥
|
||||
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型:1-自有,2-白银
|
||||
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
||||
Meta *datatypes.JSONType[any] `json:"meta" gorm:"column:meta"` // 代理服务元信息
|
||||
Version int32 `json:"version" gorm:"column:version"` // 代理服务版本
|
||||
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
||||
Host *string `json:"host,omitempty" gorm:"column:host"` // 代理服务域名
|
||||
Port *int `json:"port,omitempty" gorm:"column:port"` // 代理服务端口
|
||||
Secret *string `json:"secret,omitempty" gorm:"column:secret"` // 代理服务密钥
|
||||
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型:1-自有,2-白银
|
||||
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
||||
Meta *datatypes.JSONType[any] `json:"meta,omitempty" gorm:"column:meta"` // 代理服务元信息
|
||||
|
||||
Channels []Channel `json:"channels" gorm:"foreignkey:ProxyID"`
|
||||
Channels []Channel `json:"channels,omitempty" gorm:"foreignkey:ProxyID"`
|
||||
}
|
||||
|
||||
// ProxyType 代理服务类型枚举
|
||||
@@ -27,6 +29,7 @@ type ProxyType int
|
||||
const (
|
||||
ProxyTypeSelfHosted ProxyType = 1 // 自有
|
||||
ProxyTypeBaiYin ProxyType = 2 // 白银
|
||||
ProxyTypeGost ProxyType = 3 // GOST
|
||||
)
|
||||
|
||||
// ProxyStatus 代理服务状态枚举
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
// Refund 退款记录表
|
||||
type Refund struct {
|
||||
core.Model
|
||||
TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
||||
ProductID *int32 `json:"product_id" gorm:"column:product_id"` // 产品ID
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额
|
||||
Reason *string `json:"reason" gorm:"column:reason"` // 退款原因
|
||||
Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态:0-待处理,1-已退款,2-已拒绝
|
||||
TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
||||
ProductID *int32 `json:"product_id,omitempty" gorm:"column:product_id"` // 产品ID
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额
|
||||
Reason *string `json:"reason,omitempty" gorm:"column:reason"` // 退款原因
|
||||
Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态:0-待处理,1-已退款,2-已拒绝
|
||||
}
|
||||
|
||||
// RefundStatus 退款状态枚举
|
||||
|
||||
@@ -7,14 +7,17 @@ import (
|
||||
// Resource 套餐表
|
||||
type Resource struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ResourceNo *string `json:"resource_no" gorm:"column:resource_no"` // 套餐编号
|
||||
Active bool `json:"active" gorm:"column:active"` // 套餐状态
|
||||
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型:1-短效动态,2-长效动态
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ResourceNo *string `json:"resource_no,omitempty" gorm:"column:resource_no"` // 套餐编号
|
||||
Active bool `json:"active" gorm:"column:active"` // 套餐状态
|
||||
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型:1-短效动态,2-长效动态
|
||||
Code string `json:"code" gorm:"column:code"` // 产品编码
|
||||
CheckIP bool `json:"checkip" gorm:"column:checkip"` // 是否检查IP
|
||||
|
||||
User User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Short *ResourceShort `json:"short" gorm:"foreignKey:ResourceID"`
|
||||
Long *ResourceLong `json:"long" gorm:"foreignKey:ResourceID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Short *ResourceShort `json:"short,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
Long *ResourceLong `json:"long,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
Product *Product `json:"product,omitempty" gorm:"foreignKey:Code;references:Code"`
|
||||
}
|
||||
|
||||
// ResourceType 套餐类型枚举
|
||||
@@ -25,10 +28,32 @@ const (
|
||||
ResourceTypeLong ResourceType = 2 // 长效动态
|
||||
)
|
||||
|
||||
// ResourceLongType 套餐计费模式枚举
|
||||
func (t ResourceType) Code() string {
|
||||
switch t {
|
||||
case ResourceTypeShort:
|
||||
return "short"
|
||||
case ResourceTypeLong:
|
||||
return "long"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceMode 套餐计费模式枚举
|
||||
type ResourceMode int
|
||||
|
||||
const (
|
||||
ResourceModeTime ResourceMode = 1 // 包时
|
||||
ResourceModeQuota ResourceMode = 2 // 包量
|
||||
)
|
||||
|
||||
func (m ResourceMode) Code() string {
|
||||
switch m {
|
||||
case ResourceModeTime:
|
||||
return "time"
|
||||
case ResourceModeQuota:
|
||||
return "quota"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ import (
|
||||
|
||||
// ResourceLong 长效动态套餐表
|
||||
type ResourceLong struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // ID
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
||||
Live int32 `json:"live" gorm:"column:live"` // 可用时长(天)
|
||||
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
|
||||
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量
|
||||
Used int32 `json:"used" gorm:"column:used"` // 已用数量
|
||||
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
|
||||
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
|
||||
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
|
||||
ID int32 `json:"id" gorm:"column:id"` // ID
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
Code string `json:"code" gorm:"column:code"` // 套餐编码
|
||||
Live int32 `json:"live" gorm:"column:live"` // 可用时长(小时)
|
||||
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
||||
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
|
||||
Used int32 `json:"used" gorm:"column:used"` // 总用量
|
||||
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用量
|
||||
LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
|
||||
|
||||
Sku *ProductSku `json:"sku,omitempty" gorm:"foreignKey:Code;references:Code"`
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ import (
|
||||
|
||||
// ResourceShort 短效动态套餐表
|
||||
type ResourceShort struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // ID
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
||||
Live int32 `json:"live" gorm:"column:live"` // 可用时长(秒)
|
||||
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
|
||||
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量
|
||||
Used int32 `json:"used" gorm:"column:used"` // 已用数量
|
||||
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
|
||||
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
|
||||
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
|
||||
ID int32 `json:"id" gorm:"column:id"` // ID
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
Code string `json:"code" gorm:"column:code"` // 套餐编码
|
||||
Live int32 `json:"live" gorm:"column:live"` // 可用时长(秒)
|
||||
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
||||
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
|
||||
Used int32 `json:"used" gorm:"column:used"` // 总用量
|
||||
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用量
|
||||
LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
|
||||
|
||||
Sku *ProductSku `json:"sku,omitempty" gorm:"foreignKey:Code;references:Code"`
|
||||
}
|
||||
|
||||
@@ -9,18 +9,18 @@ import (
|
||||
// Session 会话表
|
||||
type Session struct {
|
||||
core.Model
|
||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
||||
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID
|
||||
IP *orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
|
||||
UA *string `json:"ua" gorm:"column:ua"` // 用户代理
|
||||
AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌
|
||||
AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间
|
||||
RefreshToken *string `json:"refresh_token" gorm:"column:refresh_token"` // 刷新令牌
|
||||
RefreshTokenExpires *time.Time `json:"refresh_token_expires" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间
|
||||
Scopes *string `json:"scopes" gorm:"column:scopes"` // 权限范围
|
||||
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
|
||||
ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
|
||||
IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // IP地址
|
||||
UA *string `json:"ua,omitempty" gorm:"column:ua"` // 用户代理
|
||||
AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌
|
||||
AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间
|
||||
RefreshToken *string `json:"refresh_token,omitempty" gorm:"column:refresh_token"` // 刷新令牌
|
||||
RefreshTokenExpires *time.Time `json:"refresh_token_expires,omitempty" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间
|
||||
Scopes *string `json:"scopes,omitempty" gorm:"column:scopes"` // 权限范围
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Admin *Admin `json:"admin" gorm:"foreignKey:AdminID"`
|
||||
Client *Client `json:"client" gorm:"foreignKey:ClientID;belongsTo:ID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
|
||||
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID;belongsTo:ID"`
|
||||
}
|
||||
|
||||
@@ -10,22 +10,24 @@ import (
|
||||
// Trade 订单表
|
||||
type Trade struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号
|
||||
OuterNo *string `json:"outer_no" gorm:"column:outer_no"` // 外部订单号
|
||||
Type TradeType `json:"type" gorm:"column:type"` // 订单类型:1-购买产品,2-充值余额
|
||||
Subject string `json:"subject" gorm:"column:subject"` // 订单主题
|
||||
Remark *string `json:"remark" gorm:"column:remark"` // 订单备注
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额
|
||||
Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额
|
||||
Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式:1-支付宝,2-微信,3-商福通,4-商福通渠道支付宝,5-商福通渠道微信
|
||||
Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台:1-电脑网站,2-手机网站
|
||||
Acquirer *TradeAcquirer `json:"acquirer" gorm:"column:acquirer"` // 收单机构:1-支付宝,2-微信,3-银联
|
||||
Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态:0-待支付,1-已支付,2-已取消
|
||||
Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款
|
||||
PaymentURL *string `json:"payment_url" gorm:"column:payment_url"` // 支付链接
|
||||
CompletedAt *time.Time `json:"completed_at" gorm:"column:completed_at"` // 支付时间
|
||||
CanceledAt *time.Time `json:"canceled_at" gorm:"column:canceled_at"` // 取消时间
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号
|
||||
OuterNo *string `json:"outer_no,omitempty" gorm:"column:outer_no"` // 外部订单号
|
||||
Type TradeType `json:"type" gorm:"column:type"` // 订单类型:1-购买产品,2-充值余额
|
||||
Subject string `json:"subject" gorm:"column:subject"` // 订单主题
|
||||
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 订单备注
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额
|
||||
Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额
|
||||
Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式:1-支付宝,2-微信,3-商福通,4-商福通渠道支付宝,5-商福通渠道微信
|
||||
Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台:1-电脑网站,2-手机网站
|
||||
Acquirer *TradeAcquirer `json:"acquirer,omitempty" gorm:"column:acquirer"` // 收单机构:1-支付宝,2-微信,3-银联
|
||||
Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态:0-待支付,1-已支付,2-已取消
|
||||
Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款
|
||||
PaymentURL *string `json:"payment_url,omitempty" gorm:"column:payment_url"` // 支付链接
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" gorm:"column:completed_at"` // 支付时间
|
||||
CanceledAt *time.Time `json:"canceled_at,omitempty" gorm:"column:canceled_at"` // 取消时间
|
||||
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// TradeType 订单类型枚举
|
||||
|
||||
@@ -11,25 +11,29 @@ import (
|
||||
// User 用户表
|
||||
type User struct {
|
||||
core.Model
|
||||
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
||||
Phone string `json:"phone" gorm:"column:phone"` // 手机号码
|
||||
Username *string `json:"username" gorm:"column:username"` // 用户名
|
||||
Email *string `json:"email" gorm:"column:email"` // 邮箱
|
||||
Password *string `json:"password" gorm:"column:password"` // 用户密码
|
||||
Name *string `json:"name" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL
|
||||
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态:0-禁用,1-正常
|
||||
Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额
|
||||
IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型:0-未认证,1-个人认证,2-企业认证
|
||||
IDNo *string `json:"id_no" gorm:"column:id_no"` // 身份证号或营业执照号
|
||||
IDToken *string `json:"id_token" gorm:"column:id_token"` // 身份验证标识
|
||||
ContactQQ *string `json:"contact_qq" gorm:"column:contact_qq"` // QQ联系方式
|
||||
ContactWechat *string `json:"contact_wechat" gorm:"column:contact_wechat"` // 微信联系方式
|
||||
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理
|
||||
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
|
||||
DiscountID *int32 `json:"discount_id,omitempty" gorm:"column:discount_id"` // 折扣ID
|
||||
Phone string `json:"phone" gorm:"column:phone"` // 手机号码
|
||||
Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
|
||||
Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
|
||||
Password *string `json:"-" gorm:"column:password"` // 用户密码
|
||||
Source *UserSource `json:"source,omitempty" gorm:"column:source"` // 用户来源:0-官网注册,1-管理员添加,2-代理商注册,3-代理商添加
|
||||
Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
|
||||
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态:0-禁用,1-正常
|
||||
Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额
|
||||
IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型:0-未认证,1-个人认证,2-企业认证
|
||||
IDNo *string `json:"id_no,omitempty" gorm:"column:id_no"` // 身份证号或营业执照号
|
||||
IDToken *string `json:"id_token,omitempty" gorm:"column:id_token"` // 身份验证标识
|
||||
ContactQQ *string `json:"contact_qq,omitempty" gorm:"column:contact_qq"` // QQ联系方式
|
||||
ContactWechat *string `json:"contact_wechat,omitempty" gorm:"column:contact_wechat"` // 微信联系方式
|
||||
LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
|
||||
|
||||
Admin Admin `json:"admin" gorm:"foreignKey:AdminID"`
|
||||
Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
|
||||
Roles []*UserRole `json:"roles" gorm:"many2many:link_user_role"`
|
||||
Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountID"`
|
||||
}
|
||||
|
||||
// UserStatus 用户状态枚举
|
||||
@@ -48,3 +52,13 @@ const (
|
||||
UserIDTypePersonal UserIDType = 1 // 个人认证
|
||||
UserIDTypeEnterprise UserIDType = 2 // 企业认证
|
||||
)
|
||||
|
||||
// UserSource 用户来源枚举
|
||||
type UserSource int
|
||||
|
||||
const (
|
||||
UserSourceReg UserSource = 0 // 官网注册
|
||||
UserSourceAdd UserSource = 1 // 管理员添加
|
||||
UserSourceAgentReg UserSource = 2 // 代理商注册
|
||||
UserSourceAgentAdd UserSource = 3 // 代理商添加
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user