9 Commits

47 changed files with 1217 additions and 279 deletions

57
.env.example Normal file
View File

@@ -0,0 +1,57 @@
# 应用配置
RUN_MODE=development
DEBUG_HTTP_DUMP=false
# 数据库配置
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
# 白银节点
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=

View File

@@ -19,7 +19,7 @@ WORKDIR /app
ENV TZ=Asia/Shanghai ENV TZ=Asia/Shanghai
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache ca-certificates tzdata RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /build/bin/platform_linux_amd64 /app/platform COPY --from=builder /build/bin/platform_linux_amd64 /app/platform

View File

@@ -1,8 +1,10 @@
## TODO ## TODO
交易信息持久化 proxy 的删除和更新,锁粒度应该有问题
用户请求需要检查数据权限 最低价格 0.01
交易信息持久化
用反射实现环境变量解析,以简化函数签名 用反射实现环境变量解析,以简化函数签名
@@ -10,12 +12,8 @@
分离 task 的客户端支持多进程prefork 必要!) 分离 task 的客户端支持多进程prefork 必要!)
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
慢速请求底层调用埋点监控 慢速请求底层调用埋点监控
数据库转模型文件
冷数据迁移方案 冷数据迁移方案
## 开发环境 ## 开发环境
@@ -49,13 +47,6 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
3. 异步回调事件,收到支付成功事件后自动完成订单 3. 异步回调事件,收到支付成功事件后自动完成订单
4. 用户退出支付界面,客户端主动发起关闭订单 4. 用户退出支付界面,客户端主动发起关闭订单
### 产品字典表
| 代码 | 产品 |
| ----- | ------------ |
| short | 短效动态代理 |
| long | 长效动态代理 |
### 节点分配与存储逻辑 ### 节点分配与存储逻辑
提取: 提取:

View File

@@ -31,6 +31,15 @@ services:
ports: ports:
- "5433:5432" - "5433:5432"
asynqmon:
image: hibiken/asynqmon:latest
environment:
- REDIS_ADDR=redis:6379
ports:
- "9800:8080"
depends_on:
- redis
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:

2
pkg/env/env.go vendored
View File

@@ -24,7 +24,6 @@ var (
SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时 SessionAccessExpire = 60 * 60 * 2 // 访问令牌过期时间,单位秒。默认 2 小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天 SessionRefreshExpire = 60 * 60 * 24 * 7 // 刷新令牌过期时间,单位秒。默认 7 天
DebugHttpDump = false // 是否打印请求和响应的原始数据 DebugHttpDump = false // 是否打印请求和响应的原始数据
DebugExternalChange = true // 是否实际执行外部非幂等接口调用,在开发调试时可以关闭,避免对外部数据产生影响
DbHost = "localhost" DbHost = "localhost"
DbPort = "5432" DbPort = "5432"
@@ -106,7 +105,6 @@ func Init() {
errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_EXPIRE", true, nil)) errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_EXPIRE", true, nil))
errs = append(errs, parse(&SessionRefreshExpire, "SESSION_REFRESH_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(&DebugHttpDump, "DEBUG_HTTP_DUMP", true, nil))
errs = append(errs, parse(&DebugExternalChange, "DEBUG_EXTERNAL_CHANGE", true, nil))
errs = append(errs, parse(&DbHost, "DB_HOST", true, nil)) errs = append(errs, parse(&DbHost, "DB_HOST", true, nil))
errs = append(errs, parse(&DbPort, "DB_PORT", true, nil)) errs = append(errs, parse(&DbPort, "DB_PORT", true, nil))

View File

@@ -53,6 +53,18 @@ func X[T comparable](v T) *T {
return &v 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
}
// ==================== // ====================
// 数组 // 数组
// ==================== // ====================
@@ -110,3 +122,21 @@ func CombineErrors(errs []error) error {
} }
return combinedErr 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:]
}

View File

@@ -1,13 +1,107 @@
-- ==================== -- ====================
-- region 填充数据 -- 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, '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', ''); insert into client (type, spec, name, client_id, client_secret, redirect_uri) values (1, 3, 'admin', 'admin', '$2a$10$dlfvX5Uf3iVsUWgwlb0Wt.oYsw/OEXgS.Aior3yoT63Ju7ZSsJr/2', '');
insert into product (code, name, description) values ('short', '短效动态', '短效动态'); -- ====================
insert into product (code, name, description) values ('long', '长效动态', '长效动态'); -- region 管理员
insert into product (code, name, description) values ('static', '长效静态', '长效静态'); -- ====================
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 权限 -- region 权限
@@ -32,7 +126,8 @@ insert into permission (name, description, sort) values
('channel', 'IP', 11), ('channel', 'IP', 11),
('trade', '交易', 12), ('trade', '交易', 12),
('bill', '账单', 13), ('bill', '账单', 13),
('balance_activity', '余额变动', 14); ('balance_activity', '余额变动', 14),
('proxy', '代理', 15);
-- -------------------------- -- --------------------------
-- level 2 -- level 2
@@ -95,6 +190,11 @@ 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:read', '读取 IP 列表', 1),
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '写入 IP', 2); ((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 子权限 -- trade 子权限
insert into permission (parent_id, name, description, sort) values 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:read', '读取交易列表', 1),
@@ -117,6 +217,10 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values 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); ((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);
-- resource:short 子权限 -- resource:short 子权限
insert into permission (parent_id, name, description, sort) values 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); ((select id from permission where name = 'resource:short' and deleted_at is null), 'resource:short:read', '读取用户短效动态套餐列表', 1);

View File

@@ -761,6 +761,8 @@ create table product_sku (
price decimal not null, price decimal not null,
price_min decimal not null, price_min decimal not null,
status int not null default 1, 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, created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp, updated_at timestamptz default current_timestamp,
deleted_at timestamptz deleted_at timestamptz
@@ -778,6 +780,8 @@ comment on column product_sku.code is 'SKU 代码:格式为 key=value,key=valu
comment on column product_sku.name is 'SKU 可读名称'; comment on column product_sku.name is 'SKU 可读名称';
comment on column product_sku.price_min is '最低价格'; comment on column product_sku.price_min is '最低价格';
comment on column product_sku.status is 'SKU状态0-禁用1-正常'; 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.created_at is '创建时间';
comment on column product_sku.updated_at is '更新时间'; comment on column product_sku.updated_at is '更新时间';
comment on column product_sku.deleted_at is '删除时间'; comment on column product_sku.deleted_at is '删除时间';
@@ -814,6 +818,7 @@ create table resource (
code text, code text,
type int not null, type int not null,
active bool not null default true, active bool not null default true,
checkip bool not null default true,
created_at timestamptz default current_timestamp, created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp, updated_at timestamptz default current_timestamp,
deleted_at timestamptz deleted_at timestamptz
@@ -828,9 +833,10 @@ comment on table resource is '套餐表';
comment on column resource.id is '套餐ID'; comment on column resource.id is '套餐ID';
comment on column resource.user_id is '用户ID'; comment on column resource.user_id is '用户ID';
comment on column resource.resource_no is '套餐编号'; comment on column resource.resource_no is '套餐编号';
comment on column resource.active is '套餐状态';
comment on column resource.type is '套餐类型1-短效动态2-长效动态';
comment on column resource.code 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.created_at is '创建时间';
comment on column resource.updated_at is '更新时间'; comment on column resource.updated_at is '更新时间';
comment on column resource.deleted_at is '删除时间'; comment on column resource.deleted_at is '删除时间';

81
scripts/sql/update.sql Normal file
View 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;

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"log/slog" "log/slog"
"platform/pkg/u"
"platform/web/core" "platform/web/core"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
@@ -51,7 +50,6 @@ func authUser(loginType PwdLoginType, username, password string) (user *m.User,
if user == nil { if user == nil {
user = &m.User{ user = &m.User{
Phone: username, Phone: username,
Username: u.P(username),
Status: m.UserStatusEnabled, Status: m.UserStatusEnabled,
} }
} }
@@ -79,7 +77,7 @@ func authUser(loginType PwdLoginType, username, password string) (user *m.User,
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) { func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
// 验证验证码 // 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code) err := s.Verifier.VerifySms(context.Background(), username, code, s.VerifierSmsPurposeLogin)
if err != nil { if err != nil {
return nil, core.NewBizErr("短信认证失败", err) return nil, core.NewBizErr("短信认证失败", err)
} }

View File

@@ -537,10 +537,10 @@ func introspectUser(ctx *fiber.Ctx, authCtx *AuthCtx) error {
// 掩码敏感信息 // 掩码敏感信息
if profile.Phone != "" { if profile.Phone != "" {
profile.Phone = maskPhone(profile.Phone) profile.Phone = u.MaskPhone(profile.Phone)
} }
if profile.IDNo != nil && *profile.IDNo != "" { if profile.IDNo != nil && *profile.IDNo != "" {
profile.IDNo = u.P(maskIdNo(*profile.IDNo)) profile.IDNo = u.P(u.MaskIdNo(*profile.IDNo))
} }
return ctx.JSON(struct { return ctx.JSON(struct {
@@ -579,20 +579,6 @@ func introspectAdmin(ctx *fiber.Ctx, authCtx *AuthCtx) error {
}{profile, list}) }{profile, list})
} }
func maskPhone(phone string) string {
if len(phone) < 11 {
return phone
}
return phone[:3] + "****" + phone[7:]
}
func maskIdNo(idNo string) string {
if len(idNo) < 18 {
return idNo
}
return idNo[:3] + "*********" + idNo[14:]
}
type CodeContext struct { type CodeContext struct {
UserID int32 `json:"user_id"` UserID int32 `json:"user_id"`
ClientID int32 `json:"client_id"` ClientID int32 `json:"client_id"`

View File

@@ -12,9 +12,9 @@ type IModel interface {
} }
type Model struct { type Model struct {
ID int32 `json:"id" gorm:"column:id;primaryKey"` ID int32 `json:"id,omitzero" gorm:"column:id;primaryKey"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` CreatedAt time.Time `json:"created_at,omitzero" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` UpdatedAt time.Time `json:"updated_at,omitzero" gorm:"column:updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"column:deleted_at"` DeletedAt gorm.DeletedAt `json:"-" gorm:"column:deleted_at"`
} }

View File

@@ -62,6 +62,11 @@ const (
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表 ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表
ScopeChannelWrite = string("channel:write") // 写入 IP ScopeChannelWrite = string("channel:write") // 写入 IP
ScopeProxy = string("proxy") // 代理
ScopeProxyRead = string("proxy:read") // 读取代理列表
ScopeProxyWrite = string("proxy:write") // 写入代理
ScopeProxyWriteStatus = string("proxy:write:status") // 更改代理状态
ScopeTrade = string("trade") // 交易 ScopeTrade = string("trade") // 交易
ScopeTradeRead = string("trade:read") // 读取交易列表 ScopeTradeRead = string("trade:read") // 读取交易列表
ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表 ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表

View File

@@ -15,12 +15,12 @@ func PageAdminByAdmin(c *fiber.Ctx) error {
return err return err
} }
var req PageAdminsReq var req core.PageReq
if err := g.Validator.ParseBody(c, &req); err != nil { if err := g.Validator.ParseBody(c, &req); err != nil {
return err return err
} }
list, total, err := s.Admin.Page(req.PageReq) list, total, err := s.Admin.Page(req)
if err != nil { if err != nil {
return err return err
} }
@@ -33,10 +33,6 @@ func PageAdminByAdmin(c *fiber.Ctx) error {
}) })
} }
type PageAdminsReq struct {
core.PageReq
}
func AllAdminByAdmin(c *fiber.Ctx) error { func AllAdminByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRead) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRead)
if err != nil { if err != nil {

View File

@@ -30,6 +30,9 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
if req.UserPhone != nil { if req.UserPhone != nil {
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone)) 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 { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t)) do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
@@ -68,6 +71,7 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
type PageBalanceActivityByAdminReq struct { type PageBalanceActivityByAdminReq struct {
core.PageReq core.PageReq
UserPhone *string `json:"user_phone,omitempty"` UserPhone *string `json:"user_phone,omitempty"`
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"` CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"` CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
} }
@@ -88,6 +92,9 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
// 构造查询条件 // 构造查询条件
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(req.UserID)) 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 { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t)) do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
@@ -124,6 +131,7 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
type PageBalanceActivityOfUserByAdminReq struct { type PageBalanceActivityOfUserByAdminReq struct {
core.PageReq core.PageReq
UserID int32 `json:"user_id" validate:"required"` UserID int32 `json:"user_id" validate:"required"`
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"` CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"` CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
} }

View File

@@ -201,7 +201,7 @@ func CreateChannel(c *fiber.Ctx) error {
req.AuthType == s.ChannelAuthTypeIp, req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass, req.AuthType == s.ChannelAuthTypePass,
req.Count, req.Count,
s.EdgeFilter{ &s.EdgeFilter{
Isp: isp, Isp: isp,
Prov: req.Prov, Prov: req.Prov,
City: req.City, City: req.City,

View File

@@ -34,6 +34,15 @@ func AllProductByAdmin(c *fiber.Ctx) error {
type AllProductsByAdminReq struct { 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 { func CreateProduct(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite)
if err != nil { if err != nil {

View File

@@ -1,61 +1,123 @@
package handlers package handlers
import ( import (
"net/netip"
"platform/pkg/env"
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
"platform/web/globals" g "platform/web/globals"
s "platform/web/services" s "platform/web/services"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func DebugRegisterProxyBaiYin(c *fiber.Ctx) error { func PageProxyByAdmin(c *fiber.Ctx) error {
if env.RunMode != env.RunModeDev { _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
return fiber.ErrNotFound
}
err := s.Proxy.RegisterBaiyin("1a:2b:3c:4d:5e:6f", netip.AddrFrom4([4]byte{127, 0, 0, 1}), "test", "test")
if err != nil {
return core.NewServErr("注册失败", err)
}
return nil
}
// 注册白银代理网关
func ProxyRegisterBaiYin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil { if err != nil {
return err return err
} }
req := new(RegisterProxyBaiyinReq) var req core.PageReq
err = globals.Validator.ParseBody(c, req) if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
list, total, err := s.Proxy.Page(req)
if err != nil { if err != nil {
return err return err
} }
addr, err := netip.ParseAddr(req.IP) return c.JSON(core.PageResp{
if err != nil { List: list,
return core.NewServErr("IP地址格式错误", err) Total: int(total),
} Page: req.GetPage(),
Size: req.GetSize(),
err = s.Proxy.RegisterBaiyin(req.Name, addr, req.Username, req.Password) })
if err != nil {
return core.NewServErr("注册失败", err)
}
return nil
} }
type RegisterProxyBaiyinReq struct { func AllProxyByAdmin(c *fiber.Ctx) error {
Name string `json:"name" validate:"required"` _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
IP string `json:"ip" validate:"required"` if err != nil {
Username string `json:"username" validate:"required"` return err
Password string `json:"password" validate:"required"` }
list, err := s.Proxy.All()
if err != nil {
return err
}
return c.JSON(list)
}
func CreateProxy(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
var req s.CreateProxy
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.Create(&req); err != nil {
return err
}
return c.JSON(nil)
}
func UpdateProxy(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWrite)
if err != nil {
return err
}
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 UpdateProxyStatus(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyWriteStatus)
if err != nil {
return err
}
var req s.UpdateProxyStatus
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.Proxy.UpdateStatus(&req); 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)
} }
// region 报告上线 // region 报告上线

View File

@@ -797,10 +797,7 @@ type CreateResourceReq struct {
// ResourcePrice 套餐价格 // ResourcePrice 套餐价格
func ResourcePrice(c *fiber.Ctx) error { func ResourcePrice(c *fiber.Ctx) error {
// 检查权限 // 检查权限
_, err := auth.GetAuthCtx(c).PermitSecretClient() ac := auth.GetAuthCtx(c)
if err != nil {
return err
}
// 解析请求参数 // 解析请求参数
var req = new(CreateResourceReq) var req = new(CreateResourceReq)
@@ -809,7 +806,7 @@ func ResourcePrice(c *fiber.Ctx) error {
} }
// 获取套餐价格 // 获取套餐价格
detail, err := req.TradeDetail(nil) detail, err := req.TradeDetail(ac.User)
if err != nil { if err != nil {
return err return err
} }
@@ -817,11 +814,13 @@ func ResourcePrice(c *fiber.Ctx) error {
// 计算折扣 // 计算折扣
return c.JSON(ResourcePriceResp{ return c.JSON(ResourcePriceResp{
Price: detail.Amount.StringFixed(2), Price: detail.Amount.StringFixed(2),
Discounted: detail.Actual.StringFixed(2), Discounted: detail.Discounted.StringFixed(2),
Actual: detail.Actual.StringFixed(2),
}) })
} }
type ResourcePriceResp struct { type ResourcePriceResp struct {
Price string `json:"price"` Price string `json:"price"`
Discounted string `json:"discounted_price"` Discounted string `json:"discounted"`
Actual string `json:"actual"`
} }

View File

@@ -182,6 +182,9 @@ func TradeCreate(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
if authCtx.User.IDType == m.UserIDTypeUnverified {
return core.NewBizErr("请先实名认证后再购买")
}
// 解析请求参数 // 解析请求参数
req := new(TradeCreateReq) req := new(TradeCreateReq)

View File

@@ -12,6 +12,7 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gen/field"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -305,14 +306,26 @@ func UpdateUser(c *fiber.Ctx) error {
} }
// 更新用户信息 // 更新用户信息
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))
}
_, err = q.User. _, err = q.User.
Where(q.User.ID.Eq(authCtx.User.ID)). Where(q.User.ID.Eq(authCtx.User.ID)).
Updates(m.User{ UpdateSimple(do...)
Username: &req.Username,
Email: &req.Email,
ContactQQ: &req.ContactQQ,
ContactWechat: &req.ContactWechat,
})
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
return core.NewBizErr("用户名或邮箱已被占用") return core.NewBizErr("用户名或邮箱已被占用")
} }
@@ -325,10 +338,10 @@ func UpdateUser(c *fiber.Ctx) error {
} }
type UpdateUserReq struct { type UpdateUserReq struct {
Username string `json:"username" validate:"omitempty,min=3,max=20"` Username *string `json:"username" validate:"omitempty,min=3,max=20"`
Email string `json:"email" validate:"omitempty,email"` Email *string `json:"email" validate:"omitempty,email"`
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"` ContactQQ *string `json:"contact_qq" validate:"omitempty,qq"`
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"` ContactWechat *string `json:"contact_wechat" validate:"omitempty,wechat"`
} }
// 更新账号信息 // 更新账号信息
@@ -379,16 +392,14 @@ func UpdatePassword(c *fiber.Ctx) error {
return err return err
} }
// 验证手机号
if req.Phone != authCtx.User.Phone {
return fiber.NewError(fiber.StatusBadRequest, "手机号码不正确")
}
// 验证手机令牌 // 验证手机令牌
if req.Code == "" { if req.Code == "" {
return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空") 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 { if err != nil {
return err return err
} }
@@ -411,7 +422,6 @@ func UpdatePassword(c *fiber.Ctx) error {
} }
type UpdatePasswordReq struct { type UpdatePasswordReq struct {
Phone string `json:"phone"`
Code string `json:"code"` Code string `json:"code"`
Password string `json:"password"` Password string `json:"password"`
} }

View File

@@ -5,6 +5,7 @@ import (
"platform/pkg/env" "platform/pkg/env"
"platform/web/auth" "platform/web/auth"
"platform/web/services" "platform/web/services"
s "platform/web/services"
"regexp" "regexp"
"strconv" "strconv"
@@ -13,12 +14,11 @@ import (
) )
type VerifierReq struct { type VerifierReq struct {
Purpose services.VerifierSmsPurpose `json:"purpose"` Purpose s.VerifierSmsPurpose `json:"purpose"`
Phone string `json:"phone"` Phone string `json:"phone"`
} }
func SmsCode(c *fiber.Ctx) error { func SendSmsCode(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient() _, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil { if err != nil {
return err 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 { if err != nil {
var sErr services.VerifierServiceSendLimitErr var sErr s.VerifierServiceSendLimitErr
if errors.As(err, &sErr) { if errors.As(err, &sErr) {
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr))) return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
} }
@@ -51,6 +51,23 @@ func SmsCode(c *fiber.Ctx) error {
return nil 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 { func DebugGetSmsCode(c *fiber.Ctx) error {
if env.RunMode != env.RunModeDev { if env.RunMode != env.RunModeDev {
return fiber.NewError(fiber.StatusForbidden, "not allowed") return fiber.NewError(fiber.StatusForbidden, "not allowed")

View File

@@ -12,6 +12,8 @@ type Product struct {
Description *string `json:"description,omitempty" gorm:"column:description"` // 产品描述 Description *string `json:"description,omitempty" gorm:"column:description"` // 产品描述
Sort int32 `json:"sort" gorm:"column:sort"` // 排序 Sort int32 `json:"sort" gorm:"column:sort"` // 排序
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态0-禁用1-正常 Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态0-禁用1-正常
Skus []*ProductSku `json:"skus,omitempty" gorm:"foreignKey:ProductID"` // 产品包含的SKU列表
} }
// ProductStatus 产品状态枚举 // ProductStatus 产品状态枚举

View File

@@ -16,6 +16,8 @@ type ProductSku struct {
Price decimal.Decimal `json:"price" gorm:"column:price"` // 定价 Price decimal.Decimal `json:"price" gorm:"column:price"` // 定价
PriceMin decimal.Decimal `json:"price_min" gorm:"column:price_min"` // 最低价格 PriceMin decimal.Decimal `json:"price_min" gorm:"column:price_min"` // 最低价格
Status SkuStatus `json:"status" gorm:"column:status"` // SKU 状态0-禁用1-正常 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"` Product *Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountId"` Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountId"`

View File

@@ -12,6 +12,7 @@ type Resource struct {
Active bool `json:"active" gorm:"column:active"` // 套餐状态 Active bool `json:"active" gorm:"column:active"` // 套餐状态
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型1-短效动态2-长效动态 Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型1-短效动态2-长效动态
Code string `json:"code" gorm:"column:code"` // 产品编码 Code string `json:"code" gorm:"column:code"` // 产品编码
CheckIP bool `json:"checkip" gorm:"column:checkip"` // 是否检查IP
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"` User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Short *ResourceShort `json:"short,omitempty" gorm:"foreignKey:ResourceID"` Short *ResourceShort `json:"short,omitempty" gorm:"foreignKey:ResourceID"`

View File

@@ -145,6 +145,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -173,6 +176,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -184,6 +190,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -192,8 +201,16 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
RelationField: field.NewRelation("Bill.Resource.Short.Sku", "models.ProductSku"), RelationField: field.NewRelation("Bill.Resource.Short.Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Bill.Resource.Short.Sku.Product", "models.Product"), RelationField: field.NewRelation("Bill.Resource.Short.Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Bill.Resource.Short.Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -482,6 +499,9 @@ type balanceActivityBelongsToBill struct {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -143,6 +143,9 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -154,6 +157,9 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -162,8 +168,16 @@ func newBill(db *gorm.DB, opts ...gen.DOOption) bill {
RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"), RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"), RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Resource.Short.Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -540,6 +554,9 @@ type billBelongsToResource struct {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -138,6 +138,9 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -149,6 +152,9 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -157,8 +163,16 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"), RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"), RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Resource.Short.Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -490,6 +504,9 @@ type channelBelongsToResource struct {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -128,6 +128,9 @@ func newLogsUserUsage(db *gorm.DB, opts ...gen.DOOption) logsUserUsage {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -139,6 +142,9 @@ func newLogsUserUsage(db *gorm.DB, opts ...gen.DOOption) logsUserUsage {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -147,8 +153,16 @@ func newLogsUserUsage(db *gorm.DB, opts ...gen.DOOption) logsUserUsage {
RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"), RelationField: field.NewRelation("Resource.Short.Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"), RelationField: field.NewRelation("Resource.Short.Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Resource.Short.Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -391,6 +405,9 @@ type logsUserUsageBelongsToResource struct {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -36,6 +36,29 @@ func newProduct(db *gorm.DB, opts ...gen.DOOption) product {
_product.Description = field.NewString(tableName, "description") _product.Description = field.NewString(tableName, "description")
_product.Sort = field.NewInt32(tableName, "sort") _product.Sort = field.NewInt32(tableName, "sort")
_product.Status = field.NewInt(tableName, "status") _product.Status = field.NewInt(tableName, "status")
_product.Skus = productHasManySkus{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Skus", "models.ProductSku"),
Product: struct {
field.RelationField
Skus struct {
field.RelationField
}
}{
RelationField: field.NewRelation("Skus.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Skus.Product.Skus", "models.ProductSku"),
},
},
Discount: struct {
field.RelationField
}{
RelationField: field.NewRelation("Skus.Discount", "models.ProductDiscount"),
},
}
_product.fillFieldMap() _product.fillFieldMap()
@@ -55,6 +78,7 @@ type product struct {
Description field.String Description field.String
Sort field.Int32 Sort field.Int32
Status field.Int Status field.Int
Skus productHasManySkus
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -96,7 +120,7 @@ func (p *product) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (p *product) fillFieldMap() { func (p *product) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 9) p.fieldMap = make(map[string]field.Expr, 10)
p.fieldMap["id"] = p.ID p.fieldMap["id"] = p.ID
p.fieldMap["created_at"] = p.CreatedAt p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt p.fieldMap["updated_at"] = p.UpdatedAt
@@ -106,18 +130,113 @@ func (p *product) fillFieldMap() {
p.fieldMap["description"] = p.Description p.fieldMap["description"] = p.Description
p.fieldMap["sort"] = p.Sort p.fieldMap["sort"] = p.Sort
p.fieldMap["status"] = p.Status p.fieldMap["status"] = p.Status
} }
func (p product) clone(db *gorm.DB) product { func (p product) clone(db *gorm.DB) product {
p.productDo.ReplaceConnPool(db.Statement.ConnPool) p.productDo.ReplaceConnPool(db.Statement.ConnPool)
p.Skus.db = db.Session(&gorm.Session{Initialized: true})
p.Skus.db.Statement.ConnPool = db.Statement.ConnPool
return p return p
} }
func (p product) replaceDB(db *gorm.DB) product { func (p product) replaceDB(db *gorm.DB) product {
p.productDo.ReplaceDB(db) p.productDo.ReplaceDB(db)
p.Skus.db = db.Session(&gorm.Session{})
return p return p
} }
type productHasManySkus struct {
db *gorm.DB
field.RelationField
Product struct {
field.RelationField
Skus struct {
field.RelationField
}
}
Discount struct {
field.RelationField
}
}
func (a productHasManySkus) Where(conds ...field.Expr) *productHasManySkus {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a productHasManySkus) WithContext(ctx context.Context) *productHasManySkus {
a.db = a.db.WithContext(ctx)
return &a
}
func (a productHasManySkus) Session(session *gorm.Session) *productHasManySkus {
a.db = a.db.Session(session)
return &a
}
func (a productHasManySkus) Model(m *models.Product) *productHasManySkusTx {
return &productHasManySkusTx{a.db.Model(m).Association(a.Name())}
}
func (a productHasManySkus) Unscoped() *productHasManySkus {
a.db = a.db.Unscoped()
return &a
}
type productHasManySkusTx struct{ tx *gorm.Association }
func (a productHasManySkusTx) Find() (result []*models.ProductSku, err error) {
return result, a.tx.Find(&result)
}
func (a productHasManySkusTx) Append(values ...*models.ProductSku) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a productHasManySkusTx) Replace(values ...*models.ProductSku) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a productHasManySkusTx) Delete(values ...*models.ProductSku) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a productHasManySkusTx) Clear() error {
return a.tx.Clear()
}
func (a productHasManySkusTx) Count() int64 {
return a.tx.Count()
}
func (a productHasManySkusTx) Unscoped() *productHasManySkusTx {
a.tx = a.tx.Unscoped()
return &a
}
type productDo struct{ gen.DO } type productDo struct{ gen.DO }
func (p productDo) Debug() *productDo { func (p productDo) Debug() *productDo {

View File

@@ -38,10 +38,33 @@ func newProductSku(db *gorm.DB, opts ...gen.DOOption) productSku {
_productSku.Price = field.NewField(tableName, "price") _productSku.Price = field.NewField(tableName, "price")
_productSku.PriceMin = field.NewField(tableName, "price_min") _productSku.PriceMin = field.NewField(tableName, "price_min")
_productSku.Status = field.NewInt32(tableName, "status") _productSku.Status = field.NewInt32(tableName, "status")
_productSku.Sort = field.NewInt32(tableName, "sort")
_productSku.CountMin = field.NewInt32(tableName, "count_min")
_productSku.Product = productSkuBelongsToProduct{ _productSku.Product = productSkuBelongsToProduct{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Product", "models.Product"), RelationField: field.NewRelation("Product", "models.Product"),
Skus: struct {
field.RelationField
Product struct {
field.RelationField
}
Discount struct {
field.RelationField
}
}{
RelationField: field.NewRelation("Product.Skus", "models.ProductSku"),
Product: struct {
field.RelationField
}{
RelationField: field.NewRelation("Product.Skus.Product", "models.Product"),
},
Discount: struct {
field.RelationField
}{
RelationField: field.NewRelation("Product.Skus.Discount", "models.ProductDiscount"),
},
},
} }
_productSku.Discount = productSkuBelongsToDiscount{ _productSku.Discount = productSkuBelongsToDiscount{
@@ -70,6 +93,8 @@ type productSku struct {
Price field.Field Price field.Field
PriceMin field.Field PriceMin field.Field
Status field.Int32 Status field.Int32
Sort field.Int32
CountMin field.Int32
Product productSkuBelongsToProduct Product productSkuBelongsToProduct
Discount productSkuBelongsToDiscount Discount productSkuBelongsToDiscount
@@ -100,6 +125,8 @@ func (p *productSku) updateTableName(table string) *productSku {
p.Price = field.NewField(table, "price") p.Price = field.NewField(table, "price")
p.PriceMin = field.NewField(table, "price_min") p.PriceMin = field.NewField(table, "price_min")
p.Status = field.NewInt32(table, "status") p.Status = field.NewInt32(table, "status")
p.Sort = field.NewInt32(table, "sort")
p.CountMin = field.NewInt32(table, "count_min")
p.fillFieldMap() p.fillFieldMap()
@@ -116,7 +143,7 @@ func (p *productSku) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (p *productSku) fillFieldMap() { func (p *productSku) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 13) p.fieldMap = make(map[string]field.Expr, 15)
p.fieldMap["id"] = p.ID p.fieldMap["id"] = p.ID
p.fieldMap["created_at"] = p.CreatedAt p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt p.fieldMap["updated_at"] = p.UpdatedAt
@@ -128,6 +155,8 @@ func (p *productSku) fillFieldMap() {
p.fieldMap["price"] = p.Price p.fieldMap["price"] = p.Price
p.fieldMap["price_min"] = p.PriceMin p.fieldMap["price_min"] = p.PriceMin
p.fieldMap["status"] = p.Status p.fieldMap["status"] = p.Status
p.fieldMap["sort"] = p.Sort
p.fieldMap["count_min"] = p.CountMin
} }
@@ -151,6 +180,16 @@ type productSkuBelongsToProduct struct {
db *gorm.DB db *gorm.DB
field.RelationField field.RelationField
Skus struct {
field.RelationField
Product struct {
field.RelationField
}
Discount struct {
field.RelationField
}
}
} }
func (a productSkuBelongsToProduct) Where(conds ...field.Expr) *productSkuBelongsToProduct { func (a productSkuBelongsToProduct) Where(conds ...field.Expr) *productSkuBelongsToProduct {

View File

@@ -115,8 +115,16 @@ func newProductSkuUser(db *gorm.DB, opts ...gen.DOOption) productSkuUser {
RelationField: field.NewRelation("ProductSku", "models.ProductSku"), RelationField: field.NewRelation("ProductSku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("ProductSku.Product", "models.Product"), RelationField: field.NewRelation("ProductSku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("ProductSku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -331,6 +339,9 @@ type productSkuUserBelongsToProductSku struct {
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -153,6 +153,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -181,6 +184,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -192,6 +198,9 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -200,8 +209,16 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
RelationField: field.NewRelation("Channels.Resource.Short.Sku", "models.ProductSku"), RelationField: field.NewRelation("Channels.Resource.Short.Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Channels.Resource.Short.Sku.Product", "models.Product"), RelationField: field.NewRelation("Channels.Resource.Short.Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Channels.Resource.Short.Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -387,6 +404,9 @@ type proxyHasManyChannels struct {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -36,6 +36,7 @@ func newResource(db *gorm.DB, opts ...gen.DOOption) resource {
_resource.Active = field.NewBool(tableName, "active") _resource.Active = field.NewBool(tableName, "active")
_resource.Type = field.NewInt(tableName, "type") _resource.Type = field.NewInt(tableName, "type")
_resource.Code = field.NewString(tableName, "code") _resource.Code = field.NewString(tableName, "code")
_resource.CheckIP = field.NewBool(tableName, "checkip")
_resource.Short = resourceHasOneShort{ _resource.Short = resourceHasOneShort{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@@ -44,6 +45,9 @@ func newResource(db *gorm.DB, opts ...gen.DOOption) resource {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField
@@ -52,8 +56,16 @@ func newResource(db *gorm.DB, opts ...gen.DOOption) resource {
RelationField: field.NewRelation("Short.Sku", "models.ProductSku"), RelationField: field.NewRelation("Short.Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Short.Sku.Product", "models.Product"), RelationField: field.NewRelation("Short.Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Short.Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -174,6 +186,7 @@ type resource struct {
Active field.Bool Active field.Bool
Type field.Int Type field.Int
Code field.String Code field.String
CheckIP field.Bool
Short resourceHasOneShort Short resourceHasOneShort
Long resourceHasOneLong Long resourceHasOneLong
@@ -206,6 +219,7 @@ func (r *resource) updateTableName(table string) *resource {
r.Active = field.NewBool(table, "active") r.Active = field.NewBool(table, "active")
r.Type = field.NewInt(table, "type") r.Type = field.NewInt(table, "type")
r.Code = field.NewString(table, "code") r.Code = field.NewString(table, "code")
r.CheckIP = field.NewBool(table, "checkip")
r.fillFieldMap() r.fillFieldMap()
@@ -222,7 +236,7 @@ func (r *resource) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (r *resource) fillFieldMap() { func (r *resource) fillFieldMap() {
r.fieldMap = make(map[string]field.Expr, 13) r.fieldMap = make(map[string]field.Expr, 14)
r.fieldMap["id"] = r.ID r.fieldMap["id"] = r.ID
r.fieldMap["created_at"] = r.CreatedAt r.fieldMap["created_at"] = r.CreatedAt
r.fieldMap["updated_at"] = r.UpdatedAt r.fieldMap["updated_at"] = r.UpdatedAt
@@ -232,6 +246,7 @@ func (r *resource) fillFieldMap() {
r.fieldMap["active"] = r.Active r.fieldMap["active"] = r.Active
r.fieldMap["type"] = r.Type r.fieldMap["type"] = r.Type
r.fieldMap["code"] = r.Code r.fieldMap["code"] = r.Code
r.fieldMap["checkip"] = r.CheckIP
} }
@@ -266,6 +281,9 @@ type resourceHasOneShort struct {
field.RelationField field.RelationField
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -43,8 +43,16 @@ func newResourceLong(db *gorm.DB, opts ...gen.DOOption) resourceLong {
RelationField: field.NewRelation("Sku", "models.ProductSku"), RelationField: field.NewRelation("Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Sku.Product", "models.Product"), RelationField: field.NewRelation("Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -149,6 +157,9 @@ type resourceLongHasOneSku struct {
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -43,8 +43,16 @@ func newResourceShort(db *gorm.DB, opts ...gen.DOOption) resourceShort {
RelationField: field.NewRelation("Sku", "models.ProductSku"), RelationField: field.NewRelation("Sku", "models.ProductSku"),
Product: struct { Product: struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
}{ }{
RelationField: field.NewRelation("Sku.Product", "models.Product"), RelationField: field.NewRelation("Sku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("Sku.Product.Skus", "models.ProductSku"),
},
}, },
Discount: struct { Discount: struct {
field.RelationField field.RelationField
@@ -149,6 +157,9 @@ type resourceShortHasOneSku struct {
Product struct { Product struct {
field.RelationField field.RelationField
Skus struct {
field.RelationField
}
} }
Discount struct { Discount struct {
field.RelationField field.RelationField

View File

@@ -25,7 +25,6 @@ func ApplyRouters(app *fiber.App) {
if env.RunMode == env.RunModeDev { if env.RunMode == env.RunModeDev {
debug := app.Group("/debug") debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode) debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear) debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear)
debug.Get("/session/now", func(ctx *fiber.Ctx) error { debug.Get("/session/now", func(ctx *fiber.Ctx) error {
rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now())).Find() rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now())).Find()
@@ -109,6 +108,14 @@ func userRouter(api fiber.Router) {
// 前台 // 前台
inquiry := api.Group("/inquiry") inquiry := api.Group("/inquiry")
inquiry.Post("/create", handlers.CreateInquiry) inquiry.Post("/create", handlers.CreateInquiry)
// 产品
product := api.Group("/product")
product.Post("/list", handlers.AllProduct)
// 认证
verify := api.Group("/verify")
verify.Post("/sms/password", handlers.SendSmsCodeForPassword)
} }
// 客户端接口路由 // 客户端接口路由
@@ -116,7 +123,7 @@ func clientRouter(api fiber.Router) {
client := api client := api
// 验证短信令牌 // 验证短信令牌
client.Post("/verify/sms", handlers.SmsCode) client.Post("/verify/sms", handlers.SendSmsCode)
// 套餐定价查询 // 套餐定价查询
resource := client.Group("/resource") resource := client.Group("/resource")
@@ -126,9 +133,6 @@ func clientRouter(api fiber.Router) {
channel := client.Group("/channel") channel := client.Group("/channel")
channel.Post("/remove", handlers.RemoveChannels) channel.Post("/remove", handlers.RemoveChannels)
// 代理网关注册
proxy := client.Group("/proxy")
proxy.Post("/register/baidyin", handlers.ProxyRegisterBaiYin)
} }
// 管理员接口路由 // 管理员接口路由
@@ -188,6 +192,15 @@ func adminRouter(api fiber.Router) {
channel.Post("/page", handlers.PageChannelByAdmin) channel.Post("/page", handlers.PageChannelByAdmin)
channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin) channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin)
// proxy 代理
var proxy = api.Group("/proxy")
proxy.Post("/all", handlers.AllProxyByAdmin)
proxy.Post("/page", handlers.PageProxyByAdmin)
proxy.Post("/create", handlers.CreateProxy)
proxy.Post("/update", handlers.UpdateProxy)
proxy.Post("/update/status", handlers.UpdateProxyStatus)
proxy.Post("/remove", handlers.RemoveProxy)
// trade 交易 // trade 交易
var trade = api.Group("/trade") var trade = api.Group("/trade")
trade.Post("/page", handlers.PageTradeByAdmin) trade.Post("/page", handlers.PageTradeByAdmin)

View File

@@ -16,7 +16,7 @@ func (s *billService) CreateForBalance(q *q.Query, uid, tradeId int32, detail *T
TradeID: &tradeId, TradeID: &tradeId,
Type: m.BillTypeRecharge, Type: m.BillTypeRecharge,
Info: &detail.Subject, Info: &detail.Subject,
Amount: detail.Amount, Amount: detail.Discounted,
Actual: detail.Actual, Actual: detail.Actual,
} }
@@ -37,7 +37,7 @@ func (s *billService) CreateForResource(q *q.Query, uid, resourceId int32, trade
CouponUserID: detail.CouponUserId, CouponUserID: detail.CouponUserId,
Type: m.BillTypeConsume, Type: m.BillTypeConsume,
Info: &detail.Subject, Info: &detail.Subject,
Amount: detail.Amount, Amount: detail.Discounted,
Actual: detail.Actual, Actual: detail.Actual,
} }

View File

@@ -24,7 +24,7 @@ var Channel = &channelServer{
} }
type ChannelServiceProvider interface { type ChannelServiceProvider interface {
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string) error RemoveChannels(batch string) error
} }
@@ -32,8 +32,8 @@ type channelServer struct {
provider ChannelServiceProvider provider ChannelServiceProvider
} }
func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) { func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter...) return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter)
} }
func (s *channelServer) RemoveChannels(batch string) error { func (s *channelServer) RemoveChannels(batch string) error {
@@ -87,6 +87,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
User: *resource.User, User: *resource.User,
Active: resource.Active, Active: resource.Active,
Type: resource.Type, Type: resource.Type,
CheckIP: resource.CheckIP,
} }
switch resource.Type { switch resource.Type {
@@ -94,7 +95,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
var sub = resource.Short var sub = resource.Short
info.ShortId = &sub.ID info.ShortId = &sub.ID
info.ExpireAt = sub.ExpireAt info.ExpireAt = sub.ExpireAt
info.Live = time.Duration(sub.Live) * time.Second info.Live = time.Duration(sub.Live) * time.Minute
info.Mode = sub.Type info.Mode = sub.Type
info.Quota = sub.Quota info.Quota = sub.Quota
info.Used = sub.Used info.Used = sub.Used
@@ -142,6 +143,7 @@ type ResourceView struct {
Daily int32 Daily int32
LastAt *time.Time LastAt *time.Time
Today int // 今日用量 Today int // 今日用量
CheckIP bool
} }
// 检查用户是否可提取 // 检查用户是否可提取
@@ -178,7 +180,7 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
pass = true pass = true
} }
} }
if !pass { if resource.CheckIP && !pass {
return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String())) return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
} }
@@ -232,7 +234,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
// 缩容通道 // 缩容通道
func remChans(proxy int32) error { func remChans(proxy int32) error {
key := freeChansKey + ":" + strconv.Itoa(int(proxy)) key := freeChansKey + ":" + strconv.Itoa(int(proxy))
err := g.Redis.SRem(context.Background(), key).Err() err := g.Redis.Del(context.Background(), key).Err()
if err != nil { if err != nil {
return fmt.Errorf("缩容通道失败: %w", err) return fmt.Errorf("缩容通道失败: %w", err)
} }
@@ -272,7 +274,8 @@ local free_key = KEYS[1]
local batch_key = KEYS[2] local batch_key = KEYS[2]
local count = tonumber(ARGV[1]) local count = tonumber(ARGV[1])
if redis.call("SCARD", free_key) < count then local free_count = redis.call("SCARD", free_key)
if count <= 0 or free_count < count then
return nil return nil
end end
@@ -305,12 +308,13 @@ local free_key = KEYS[1]
local batch_key = KEYS[2] local batch_key = KEYS[2]
local chans = redis.call("LRANGE", batch_key, 0, -1) local chans = redis.call("LRANGE", batch_key, 0, -1)
redis.call("DEL", batch_key) if #chans == 0 then
return 1
if redis.call("EXISTS", free_key) == 1 then
redis.call("SADD", free_key, unpack(chans))
end end
redis.call("SADD", free_key, unpack(chans))
redis.call("DEL", batch_key)
return 1 return 1
`) `)

View File

@@ -23,10 +23,9 @@ import (
type channelBaiyinProvider struct{} type channelBaiyinProvider struct{}
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) { func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) {
var filter *EdgeFilter = nil if filter == nil {
if len(edgeFilter) > 0 { return nil, core.NewBizErr("缺少节点过滤条件")
filter = &edgeFilter[0]
} }
now := time.Now() now := time.Now()
@@ -64,10 +63,27 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
} }
proxy := proxyResult.Proxy proxy := proxyResult.Proxy
// 获取可用通道 // 锁内确认状态并锁定端口,避免与状态切换并发穿透
chans, err := lockChans(proxy.ID, batch, count) var chans []netip.AddrPort
err = g.Redsync.WithLock(proxyStatusLockKey(proxy.ID), func() error {
lockedProxy, err := q.Proxy.Where(q.Proxy.ID.Eq(proxy.ID)).Take()
if err != nil { if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err) return err
}
if lockedProxy.Status != m.ProxyStatusOnline {
return core.NewBizErr("无可用主机,请稍后再试")
}
chans, err = lockChans(proxy.ID, batch, count)
if err != nil {
return core.NewBizErr("无可用通道,请稍后再试", err)
}
proxy = *lockedProxy
return nil
})
if err != nil {
return nil, err
} }
// 获取可用节点 // 获取可用节点
@@ -86,7 +102,7 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
return nil, core.NewBizErr("获取可用节点失败", err) return nil, core.NewBizErr("获取可用节点失败", err)
} }
if edgesResp.Total != count && len(edgesResp.Edges) != count { if edgesResp.Total != count && len(edgesResp.Edges) != count {
return nil, core.NewBizErr("地区可用节点数量不足 [%s, %s] [%s]") return nil, core.NewBizErr("地区可用节点数量不足")
} }
edges := edgesResp.Edges edges := edgesResp.Edges
@@ -98,10 +114,6 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
ch := chans[i] ch := chans[i]
edge := edges[i] edge := edges[i]
if err != nil {
return nil, core.NewBizErr("解析通道地址失败", err)
}
// 通道数据 // 通道数据
channels[i] = &m.Channel{ channels[i] = &m.Channel{
UserID: user.ID, UserID: user.ID,
@@ -229,7 +241,7 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
// 提交配置 // 提交配置
secret := strings.Split(u.Z(proxy.Secret), ":") secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1]) gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
if env.DebugExternalChange { if env.RunMode == env.RunModeProd {
// 连接节点到网关 // 连接节点到网关
err = g.Cloud.CloudConnect(&g.CloudConnectReq{ err = g.Cloud.CloudConnect(&g.CloudConnectReq{
@@ -292,7 +304,8 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
} }
// 提交配置 // 提交配置
if env.DebugExternalChange { if env.RunMode == env.RunModeProd {
// 断开节点连接 // 断开节点连接
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{ g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac, Uuid: proxy.Mac,
@@ -300,7 +313,7 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
}) })
// 清空通道配置 // 清空通道配置
secret := strings.Split(*proxy.Secret, ":") secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1]) gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
err := gateway.GatewayPortConfigs(configs) err := gateway.GatewayPortConfigs(configs)
if err != nil { if err != nil {
@@ -320,6 +333,6 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
return err return err
} }
slog.Debug("清除代理端口配置", "time", time.Since(start).String()) slog.Debug("清除代理端口配置", "duration", time.Since(start).String())
return nil return nil
} }

View File

@@ -20,6 +20,62 @@ func (s *productService) AllProducts() ([]*m.Product, error) {
Find() Find()
} }
func (s *productService) AllProductSaleInfos() ([]*m.Product, error) {
products, err := q.Product.
Select(
q.Product.ID,
q.Product.Code,
q.Product.Name,
q.Product.Description,
q.Product.Sort,
).
Where(
q.Product.Status.Eq(int(m.ProductStatusEnabled)),
).
Order(q.Product.Sort).
Find()
if err != nil {
return nil, err
}
pids := make([]int32, len(products))
for i, p := range products {
pids[i] = p.ID
}
skus, err := q.ProductSku.
Select(
q.ProductSku.ID,
q.ProductSku.ProductID,
q.ProductSku.Name,
q.ProductSku.Code,
q.ProductSku.Price,
q.ProductSku.CountMin,
).
Where(
q.ProductSku.ProductID.In(pids...),
q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled)),
).
Order(q.ProductSku.Sort).
Find()
if err != nil {
return nil, err
}
pmap := make(map[int32]*m.Product, len(products))
for _, p := range products {
pmap[p.ID] = p
p.Skus = make([]*m.ProductSku, 0)
}
for _, s := range skus {
if p, ok := pmap[s.ProductID]; ok {
p.Skus = append(p.Skus, s)
}
}
return products, nil
}
// 新增产品 // 新增产品
func (s *productService) CreateProduct(create *CreateProductData) error { func (s *productService) CreateProduct(create *CreateProductData) error {
return q.Product.Create(&m.Product{ return q.Product.Create(&m.Product{

View File

@@ -20,7 +20,7 @@ func (s *productSkuService) All(product_code string) (result []*m.ProductSku, er
Joins(q.ProductSku.Product). Joins(q.ProductSku.Product).
Where(q.Product.As("Product").Code.Eq(product_code)). Where(q.Product.As("Product").Code.Eq(product_code)).
Select(q.ProductSku.ALL). Select(q.ProductSku.ALL).
Order(q.ProductSku.CreatedAt.Desc()). Order(q.ProductSku.Sort).
Find() Find()
} }
@@ -32,7 +32,7 @@ func (s *productSkuService) Page(req *core.PageReq, productId *int32) (result []
return q.ProductSku. return q.ProductSku.
Joins(q.ProductSku.Discount, q.ProductSku.Product). Joins(q.ProductSku.Discount, q.ProductSku.Product).
Where(do...). Where(do...).
Order(q.ProductSku.ID). Order(q.ProductSku.Sort).
FindByPage(req.GetOffset(), req.GetLimit()) FindByPage(req.GetOffset(), req.GetLimit())
} }
@@ -47,6 +47,11 @@ func (s *productSkuService) Create(create CreateProductSkuData) (err error) {
return core.NewBizErr("产品最低价格的格式不正确", err) return core.NewBizErr("产品最低价格的格式不正确", err)
} }
countMin := int32(1)
if create.CountMin != nil {
countMin = *create.CountMin
}
return q.ProductSku.Create(&m.ProductSku{ return q.ProductSku.Create(&m.ProductSku{
ProductID: create.ProductID, ProductID: create.ProductID,
DiscountId: create.DiscountID, DiscountId: create.DiscountID,
@@ -54,6 +59,8 @@ func (s *productSkuService) Create(create CreateProductSkuData) (err error) {
Name: create.Name, Name: create.Name,
Price: price, Price: price,
PriceMin: priceMin, PriceMin: priceMin,
Sort: create.Sort,
CountMin: countMin,
}) })
} }
@@ -64,6 +71,8 @@ type CreateProductSkuData struct {
Name string `json:"name"` Name string `json:"name"`
Price string `json:"price"` Price string `json:"price"`
PriceMin string `json:"price_min"` PriceMin string `json:"price_min"`
Sort int32 `json:"sort"`
CountMin *int32 `json:"count_min"`
} }
func (s *productSkuService) Update(update UpdateProductSkuData) (err error) { func (s *productSkuService) Update(update UpdateProductSkuData) (err error) {
@@ -95,6 +104,12 @@ func (s *productSkuService) Update(update UpdateProductSkuData) (err error) {
if update.Status != nil { if update.Status != nil {
do = append(do, q.ProductSku.Status.Value(*update.Status)) do = append(do, q.ProductSku.Status.Value(*update.Status))
} }
if update.Sort != nil {
do = append(do, q.ProductSku.Sort.Value(*update.Sort))
}
if update.CountMin != nil {
do = append(do, q.ProductSku.CountMin.Value(*update.CountMin))
}
_, err = q.ProductSku.Where(q.ProductSku.ID.Eq(update.ID)).UpdateSimple(do...) _, err = q.ProductSku.Where(q.ProductSku.ID.Eq(update.ID)).UpdateSimple(do...)
return err return err
@@ -108,6 +123,8 @@ type UpdateProductSkuData struct {
Price *string `json:"price"` Price *string `json:"price"`
PriceMin string `json:"price_min"` PriceMin string `json:"price_min"`
Status *int32 `json:"status"` Status *int32 `json:"status"`
Sort *int32 `json:"sort"`
CountMin *int32 `json:"count_min"`
} }
func (s *productSkuService) Delete(id int32) (err error) { func (s *productSkuService) Delete(id int32) (err error) {

View File

@@ -1,65 +1,216 @@
package services package services
import ( import (
"context"
"fmt" "fmt"
"net/netip" "net/netip"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
g "platform/web/globals"
"platform/web/globals/orm" "platform/web/globals/orm"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strconv"
"time" "time"
"gorm.io/gen/field"
) )
var Proxy = &proxyService{} var Proxy = &proxyService{}
type proxyService struct{} type proxyService struct{}
// AllProxies 获取所有代理 func proxyStatusLockKey(id int32) string {
func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Proxy, error) { return fmt.Sprintf("platform:proxy:status:%d", id)
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(proxyType)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
).Preload(
q.Proxy.Channels.On(q.Channel.ExpiredAt.Gte(time.Now())),
).Find()
if err != nil {
return nil, err
}
return proxies, nil
} }
// RegisterBaiyin 注册新代理服务 func hasUsedChans(proxyID int32) (bool, error) {
func (s *proxyService) RegisterBaiyin(Name string, IP netip.Addr, username, password string) error { ctx := context.Background()
pattern := usedChansKey + ":" + strconv.Itoa(int(proxyID)) + ":*"
// 保存代理信息 keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result()
proxy := &m.Proxy{ if err != nil {
Version: 0, return false, err
Mac: Name,
IP: orm.Inet{Addr: IP},
Secret: u.P(fmt.Sprintf("%s:%s", username, password)),
Type: m.ProxyTypeBaiYin,
Status: m.ProxyStatusOnline,
} }
if err := q.Proxy.Create(proxy); err != nil { return len(keys) > 0, nil
return core.NewServErr("保存通道数据失败") }
func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
if err := remChans(proxyID); err != nil {
return err
} }
// 添加可用通道到 redis
chans := make([]netip.AddrPort, 10000) chans := make([]netip.AddrPort, 10000)
for i := range 10000 { for i := range 10000 {
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000)) chans[i] = netip.AddrPortFrom(addr, uint16(i+10000))
} }
err := regChans(proxy.ID, chans)
if err := regChans(proxyID, chans); err != nil {
return err
}
return nil
}
func (s *proxyService) Page(req core.PageReq) (result []*m.Proxy, count int64, err error) {
return q.Proxy.
Omit(q.Proxy.Version, q.Proxy.Meta).
Order(q.Proxy.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
}
func (s *proxyService) All() (result []*m.Proxy, err error) {
return q.Proxy.
Omit(q.Proxy.Version, q.Proxy.Meta).
Order(q.Proxy.CreatedAt.Desc()).
Find()
}
type CreateProxy struct {
Mac string `json:"mac" validate:"required"`
IP string `json:"ip" validate:"required"`
Host *string `json:"host"`
Secret *string `json:"secret"`
Type *m.ProxyType `json:"type"`
Status *m.ProxyStatus `json:"status"`
}
func (s *proxyService) Create(create *CreateProxy) error {
addr, err := netip.ParseAddr(create.IP)
if err != nil { if err != nil {
return core.NewServErr("添加通道失败", err) return core.NewServErr("IP地址格式错误", err)
} }
return q.Q.Transaction(func(tx *q.Query) error {
proxy := &m.Proxy{
Mac: create.Mac,
IP: orm.Inet{Addr: addr},
Host: create.Host,
Secret: create.Secret,
Type: u.Else(create.Type, m.ProxyTypeSelfHosted),
Status: u.Else(create.Status, m.ProxyStatusOffline),
}
if err := tx.Proxy.Create(proxy); err != nil {
return core.NewServErr("保存代理数据失败", err)
}
if err := rebuildFreeChans(proxy.ID, addr); err != nil {
return core.NewServErr("初始化代理通道失败", err)
}
return nil
})
}
type UpdateProxy struct {
ID int32 `json:"id" validate:"required"`
Mac *string `json:"mac"`
IP *string `json:"ip"`
Host *string `json:"host"`
Secret *string `json:"secret"`
}
func (s *proxyService) Update(update *UpdateProxy) error {
simples := make([]field.AssignExpr, 0)
hasSideEffect := false
if update.Mac != nil {
hasSideEffect = true
simples = append(simples, q.Proxy.Mac.Value(*update.Mac))
}
if update.IP != nil {
addr, err := netip.ParseAddr(*update.IP)
if err != nil {
return core.NewServErr("IP地址格式错误", err)
}
hasSideEffect = true
simples = append(simples, q.Proxy.IP.Value(orm.Inet{Addr: addr}))
}
if update.Host != nil {
simples = append(simples, q.Proxy.Host.Value(*update.Host))
}
if update.Secret != nil {
hasSideEffect = true
simples = append(simples, q.Proxy.Secret.Value(*update.Secret))
}
if len(simples) == 0 {
return nil
}
if hasSideEffect {
used, err := hasUsedChans(update.ID)
if err != nil {
return core.NewServErr("检查代理通道状态失败", err)
}
if used {
return core.NewBizErr("代理存在未关闭通道,禁止修改")
}
}
rs, err := q.Proxy.
Where(
q.Proxy.ID.Eq(update.ID),
q.Proxy.Status.Eq(int(m.ProxyStatusOffline)),
).
UpdateSimple(simples...)
if err != nil {
return err
}
if rs.RowsAffected == 0 {
return core.NewBizErr("代理未下线,禁止修改")
}
return nil return nil
} }
// UnregisterBaiyin 注销代理服务 func (s *proxyService) Remove(id int32) error {
func (s *proxyService) UnregisterBaiyin(id int) error { used, err := hasUsedChans(id)
if err != nil {
return core.NewServErr("检查代理通道状态失败", err)
}
if used {
return core.NewBizErr("代理存在未关闭通道,禁止删除")
}
rs, err := q.Proxy.
Where(
q.Proxy.ID.Eq(id),
q.Proxy.Status.Eq(int(m.ProxyStatusOffline)),
).
UpdateColumn(q.Proxy.DeletedAt, time.Now())
if err != nil {
return err
}
if rs.RowsAffected == 0 {
return core.NewBizErr("代理未下线,禁止删除")
}
if err := remChans(id); err != nil {
return core.NewServErr("注销代理通道失败", err)
}
return nil return nil
} }
type UpdateProxyStatus struct {
ID int32 `json:"id" validate:"required"`
Status m.ProxyStatus `json:"status"`
}
func (s *proxyService) UpdateStatus(update *UpdateProxyStatus) error {
return g.Redsync.WithLock(proxyStatusLockKey(update.ID), func() error {
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(update.ID)).Take()
if err != nil {
return err
}
if proxy.Status == update.Status {
return nil
}
if update.Status == m.ProxyStatusOnline {
if err := rebuildFreeChans(proxy.ID, proxy.IP.Addr); err != nil {
return core.NewServErr("初始化代理通道失败", err)
}
}
_, err = q.Proxy.
Where(q.Proxy.ID.Eq(update.ID)).
UpdateSimple(q.Proxy.Status.Value(int(update.Status)))
return err
})
}

View File

@@ -2,6 +2,7 @@ package services
import ( import (
"fmt" "fmt"
"log/slog"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
m "platform/web/models" m "platform/web/models"
@@ -120,14 +121,13 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
} }
func (s *resourceService) Update(data *UpdateResourceData) error { func (s *resourceService) Update(data *UpdateResourceData) error {
if data.Active == nil {
return core.NewBizErr("更新套餐失败active 不能为空")
}
do := make([]field.AssignExpr, 0) do := make([]field.AssignExpr, 0)
if data.Active != nil { if data.Active != nil {
do = append(do, q.Resource.Active.Value(*data.Active)) do = append(do, q.Resource.Active.Value(*data.Active))
} }
if data.CheckIP != nil {
do = append(do, q.Resource.CheckIP.Value(*data.CheckIP))
}
_, err := q.Resource. _, err := q.Resource.
Where(q.Resource.ID.Eq(data.Id)). Where(q.Resource.ID.Eq(data.Id)).
@@ -142,37 +142,42 @@ func (s *resourceService) Update(data *UpdateResourceData) error {
type UpdateResourceData struct { type UpdateResourceData struct {
core.IdReq core.IdReq
Active *bool `json:"active"` Active *bool `json:"active"`
CheckIP *bool `json:"checkip"`
} }
func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, cuid *int32) (*m.ProductSku, *m.ProductDiscount, *m.CouponUser, decimal.Decimal, decimal.Decimal, error) { func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, cuid *int32) (*m.ProductSku, *m.ProductDiscount, *m.CouponUser, decimal.Decimal, decimal.Decimal, decimal.Decimal, error) {
if count <= 0 {
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr("购买数量必须大于 0")
}
sku, err := q.ProductSku. sku, err := q.ProductSku.
Joins(q.ProductSku.Discount). Joins(q.ProductSku.Discount).
Where(q.ProductSku.Code.Eq(skuCode), q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled))). Where(q.ProductSku.Code.Eq(skuCode), q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled))).
Take() Take()
if err != nil { if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("产品不可用", err) slog.Debug("查询产品失败", "skuCode", skuCode)
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr("产品不可用", err)
} }
// 原价 // 原价
price := sku.Price amountMin := sku.PriceMin.Mul(decimal.NewFromInt32(count))
amount := price.Mul(decimal.NewFromInt32(count)) amount := sku.Price.Mul(decimal.NewFromInt32(count))
// 折扣价 // 折扣价
discount := sku.Discount discount := sku.Discount
if discount == nil { if discount == nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("价格查询失败", err) return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("产品未配置折扣", err)
} }
discountRate := discount.Rate() discountRate := discount.Rate()
if user != nil && user.DiscountID != nil { // 用户特殊优惠 if user != nil && user.DiscountID != nil { // 用户特殊优惠
uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take() uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take()
if err != nil { if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err) return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err)
} }
uDiscountRate := uDiscount.Rate() uDiscountRate := uDiscount.Rate()
if uDiscountRate.Cmp(discountRate) > 0 { if uDiscountRate.Cmp(discountRate) < 0 {
discountRate = uDiscountRate discountRate = uDiscountRate
discount = uDiscount discount = uDiscount
} }
@@ -186,20 +191,20 @@ func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, c
var err error var err error
coupon, err = Coupon.GetUserCoupon(user.ID, *cuid, discounted) coupon, err = Coupon.GetUserCoupon(user.ID, *cuid, discounted)
if err != nil { if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, err return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, err
} }
couponApplied = discounted.Sub(coupon.Coupon.Amount) couponApplied = discounted.Sub(coupon.Coupon.Amount)
} }
// 约束到最低价格 // 约束到最低价格
if discounted.Cmp(sku.PriceMin) < 0 { if discounted.Cmp(amountMin) < 0 {
discounted = sku.PriceMin.Copy() discounted = amountMin.Copy()
} }
if couponApplied.Cmp(sku.PriceMin) < 0 { if couponApplied.Cmp(amountMin) < 0 {
couponApplied = sku.PriceMin.Copy() couponApplied = amountMin.Copy()
} }
return sku, discount, coupon, discounted, couponApplied, nil return sku, discount, coupon, amount.RoundCeil(2), discounted.RoundCeil(2), couponApplied.RoundCeil(2), nil
} }
type CreateResourceData struct { type CreateResourceData struct {
@@ -260,7 +265,7 @@ func (data *CreateResourceData) Code() string {
} }
func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) { func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) {
sku, discount, coupon, amount, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponId) sku, discount, coupon, amount, discounted, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -277,7 +282,7 @@ func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error)
data, data,
m.TradeTypePurchase, m.TradeTypePurchase,
sku.Name, sku.Name,
amount, actual, amount, discounted, actual,
discountId, couponUserId, discountId, couponUserId,
}, nil }, nil
} }

View File

@@ -619,6 +619,7 @@ type TradeDetail struct {
Type m.TradeType `json:"type"` Type m.TradeType `json:"type"`
Subject string `json:"subject"` Subject string `json:"subject"`
Amount decimal.Decimal `json:"amount"` Amount decimal.Decimal `json:"amount"`
Discounted decimal.Decimal `json:"discounted"`
Actual decimal.Decimal `json:"actual"` Actual decimal.Decimal `json:"actual"`
DiscountId *int32 `json:"discount_id,omitempty"` DiscountId *int32 `json:"discount_id,omitempty"`
CouponUserId *int32 `json:"coupon_id,omitempty"` CouponUserId *int32 `json:"coupon_id,omitempty"`

View File

@@ -88,12 +88,15 @@ type UpdateBalanceData struct {
} }
func (data *UpdateBalanceData) TradeDetail(user *m.User) (*TradeDetail, error) { func (data *UpdateBalanceData) TradeDetail(user *m.User) (*TradeDetail, error) {
if data.Amount <= 0 {
return nil, core.NewBizErr("充值金额必须大于0")
}
amount := decimal.NewFromInt(int64(data.Amount)).Div(decimal.NewFromInt(100)) amount := decimal.NewFromInt(int64(data.Amount)).Div(decimal.NewFromInt(100))
return &TradeDetail{ return &TradeDetail{
data, data,
m.TradeTypeRecharge, m.TradeTypeRecharge,
fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)), fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)),
amount, amount, amount, amount, amount,
nil, nil, nil, nil,
}, nil }, nil
} }

View File

@@ -55,7 +55,7 @@ func (s *verifierService) SendSms(ctx context.Context, phone string, purpose Ver
code := rand.Intn(900000) + 100000 // 6-digit code between 100000-999999 code := rand.Intn(900000) + 100000 // 6-digit code between 100000-999999
// 发送短信验证码 // 发送短信验证码
if env.DebugExternalChange { if env.RunMode == env.RunModeProd {
params, err := json.Marshal(map[string]string{ params, err := json.Marshal(map[string]string{
"code": strconv.Itoa(code), "code": strconv.Itoa(code),
}) })
@@ -89,8 +89,8 @@ func (s *verifierService) SendSms(ctx context.Context, phone string, purpose Ver
return nil return nil
} }
func (s *verifierService) VerifySms(ctx context.Context, phone, code string) error { func (s *verifierService) VerifySms(ctx context.Context, phone, code string, purpose VerifierSmsPurpose) error {
key := smsKey(phone, VerifierSmsPurposeLogin) key := smsKey(phone, purpose)
keyLock := key + ":lock" keyLock := key + ":lock"
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error { err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
@@ -147,6 +147,7 @@ type VerifierSmsPurpose int
const ( const (
VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录 VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录
VerifierSmsPurposePassword // 修改密码
) )
// region 服务异常 // region 服务异常