16 Commits

56 changed files with 2107 additions and 323 deletions

View File

@@ -19,7 +19,7 @@ WORKDIR /app
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
COPY --from=builder /build/bin/platform_linux_amd64 /app/platform

View File

@@ -1,8 +1,18 @@
## TODO
交易信息持久化
- 选择代理部分,可以检查 redis 中的可用端口数量,无需查数据库
- 取用端口后直接写入关闭任务,避免中途失败导致端口泄漏
-
用户请求需要检查数据权限
---
错误提示增强,展示整链路信息
ip 提取频率限制,在 ensure 函数加逻辑,通过 redis 或者 pg 计算分钟内提取次数,只允许每分钟提取 30 次
proxy 的删除和更新,锁粒度应该有问题
交易信息持久化
用反射实现环境变量解析,以简化函数签名
@@ -10,12 +20,8 @@
分离 task 的客户端支持多进程prefork 必要!)
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
慢速请求底层调用埋点监控
数据库转模型文件
冷数据迁移方案
## 开发环境
@@ -49,13 +55,6 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
3. 异步回调事件,收到支付成功事件后自动完成订单
4. 用户退出支付界面,客户端主动发起关闭订单
### 产品字典表
| 代码 | 产品 |
| ----- | ------------ |
| short | 短效动态代理 |
| long | 长效动态代理 |
### 节点分配与存储逻辑
提取:

View File

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

View File

@@ -53,6 +53,18 @@ func X[T comparable](v T) *T {
return &v
}
// N 零值视为 nil
func N[T comparable](v *T) *T {
if v == nil {
return nil
}
var zero T
if *v == zero {
return nil
}
return v
}
// ====================
// 数组
// ====================
@@ -110,3 +122,21 @@ func CombineErrors(errs []error) error {
}
return combinedErr
}
// ====================
// 业务
// ====================
func MaskPhone(phone string) string {
if len(phone) < 11 {
return phone
}
return phone[:3] + "****" + phone[7:]
}
func MaskIdNo(idNo string) string {
if len(idNo) < 18 {
return idNo
}
return idNo[:3] + "*********" + idNo[14:]
}

View File

@@ -15,9 +15,93 @@ insert into admin (username, password, name, lock) values ('admin', '', '超级
-- region 产品
-- ====================
insert into product (code, name, description) values ('short', '短效动态', '短效动态');
insert into product (code, name, description) values ('long', '长效动态', '长效动态');
insert into product (code, name, description) values ('static', '长效静', '长效静');
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 权限
@@ -42,7 +126,9 @@ insert into permission (name, description, sort) values
('channel', 'IP', 11),
('trade', '交易', 12),
('bill', '账单', 13),
('balance_activity', '余额变动', 14);
('balance_activity', '余额变动', 14),
('proxy', '代理', 15),
('coupon_user', '已发放优惠券', 16);
-- --------------------------
-- level 2
@@ -51,74 +137,84 @@ insert into permission (name, description, sort) values
-- permission 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'permission' and deleted_at is null), 'permission:read', '读取权限列表', 1),
((select id from permission where name = 'permission' and deleted_at is null), 'permission:write', '写入权限', 2);
((select id from permission where name = 'permission' and deleted_at is null), 'permission:write', '编辑权限', 2);
-- admin_role 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:read', '读取管理员角色列表', 1),
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:write', '写入管理员角色', 2);
((select id from permission where name = 'admin_role' and deleted_at is null), 'admin_role:write', '编辑管理员角色', 2);
-- admin 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'admin' and deleted_at is null), 'admin:read', '读取管理员列表', 1),
((select id from permission where name = 'admin' and deleted_at is null), 'admin:write', '写入管理员', 2);
((select id from permission where name = 'admin' and deleted_at is null), 'admin:write', '编辑管理员', 2);
-- product 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'product' and deleted_at is null), 'product:read', '读取产品列表', 1),
((select id from permission where name = 'product' and deleted_at is null), 'product:write', '写入产品', 2);
((select id from permission where name = 'product' and deleted_at is null), 'product:write', '编辑产品', 2);
-- product_sku 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:read', '读取产品套餐列表', 1),
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:write', '写入产品套餐', 2);
((select id from permission where name = 'product_sku' and deleted_at is null), 'product_sku:write', '编辑产品套餐', 2);
-- discount 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'discount' and deleted_at is null), 'discount:read', '读取折扣列表', 1),
((select id from permission where name = 'discount' and deleted_at is null), 'discount:write', '写入折扣', 2);
((select id from permission where name = 'discount' and deleted_at is null), 'discount:write', '编辑折扣', 2);
-- resource 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'resource' and deleted_at is null), 'resource:read', '读取用户套餐列表', 1),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:write', '写入用户套餐', 2),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:write', '编辑用户套餐', 2),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:short', '短效动态套餐', 3),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4);
-- user 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'user' and deleted_at is null), 'user:read', '读取用户列表', 1),
((select id from permission where name = 'user' and deleted_at is null), 'user:write', '写入用户', 2);
((select id from permission where name = 'user' and deleted_at is null), 'user:write', '编辑用户', 2);
-- coupon 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:read', '读取优惠券列表', 1),
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:write', '写入优惠券', 2);
((select id from permission where name = 'coupon' and deleted_at is null), 'coupon:write', '编辑优惠券', 2);
-- batch 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'batch' and deleted_at is null), 'batch:read', '读取批次列表', 1),
((select id from permission where name = 'batch' and deleted_at is null), 'batch:write', '写入批次', 2);
((select id from permission where name = 'batch' and deleted_at is null), 'batch:write', '编辑批次', 2);
-- channel 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'channel' and deleted_at is null), 'channel:read', '读取 IP 列表', 1),
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '写入 IP', 2);
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '编辑 IP', 2);
-- proxy 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:read', '读取代理列表', 1),
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:write', '编辑代理', 2);
-- trade 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'trade' and deleted_at is null), 'trade:read', '读取交易列表', 1),
((select id from permission where name = 'trade' and deleted_at is null), 'trade:write', '写入交易', 2);
((select id from permission where name = 'trade' and deleted_at is null), 'trade:write', '编辑交易', 2);
-- bill 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'bill' and deleted_at is null), 'bill:read', '读取账单列表', 1),
((select id from permission where name = 'bill' and deleted_at is null), 'bill:write', '写入账单', 2);
((select id from permission where name = 'bill' and deleted_at is null), 'bill:write', '编辑账单', 2);
-- balance_activity 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'balance_activity' and deleted_at is null), 'balance_activity:read', '读取余额变动列表', 1);
-- coupon_user 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:read', '读取已发放优惠券列表', 1),
((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:write', '编辑已发放优惠券', 2);
-- --------------------------
-- level 3
-- --------------------------
@@ -127,6 +223,10 @@ 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);
-- 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 子权限
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);
@@ -142,7 +242,7 @@ insert into permission (parent_id, name, description, sort) values
-- user:write 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:balance', '写入用户余额', 1),
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:balance', '编辑用户余额', 1),
((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:bind', '用户认领', 2);
-- batch:read 子权限
@@ -165,6 +265,14 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'balance_activity:read' and deleted_at is null), 'balance_activity:read:of_user', '读取指定用户的余额变动列表', 1);
-- coupon:write 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon:write' and deleted_at is null), 'coupon:write:assign', '发放优惠券', 1);
-- coupon_user:read 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'coupon_user:read' and deleted_at is null), 'coupon_user:read:of_user', '读取指定用户的已发放优惠券列表', 1);
-- --------------------------
-- level 4
-- --------------------------

View File

@@ -761,6 +761,8 @@ create table product_sku (
price decimal not null,
price_min decimal not null,
status int not null default 1,
sort int not null default 0,
count_min int not null default 1,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
@@ -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.price_min is '最低价格';
comment on column product_sku.status is 'SKU状态0-禁用1-正常';
comment on column product_sku.sort is '排序';
comment on column product_sku.count_min is '最小购买数量';
comment on column product_sku.created_at is '创建时间';
comment on column product_sku.updated_at is '更新时间';
comment on column product_sku.deleted_at is '删除时间';
@@ -814,6 +818,7 @@ create table resource (
code text,
type int not null,
active bool not null default true,
checkip bool not null default true,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
@@ -828,9 +833,10 @@ comment on table resource is '套餐表';
comment on column resource.id is '套餐ID';
comment on column resource.user_id is '用户ID';
comment on column resource.resource_no is '套餐编号';
comment on column resource.active is '套餐状态';
comment on column resource.type is '套餐类型1-短效动态2-长效动态';
comment on column resource.code is '产品编码';
comment on column resource.type is '套餐类型1-短效动态2-长效动态';
comment on column resource.active is '套餐状态';
comment on column resource.checkip is '提取时是否检查 ip 地址';
comment on column resource.created_at is '创建时间';
comment on column resource.updated_at is '更新时间';
comment on column resource.deleted_at is '删除时间';
@@ -1105,7 +1111,7 @@ comment on table coupon_user is '优惠券发放表';
comment on column coupon_user.id is '记录ID';
comment on column coupon_user.coupon_id is '优惠券ID';
comment on column coupon_user.user_id is '用户ID';
comment on column coupon_user.status is '使用状态0-未使用1-已使用';
comment on column coupon_user.status is '使用状态0-未使用1-已使用2-已禁用';
comment on column coupon_user.expire_at is '过期时间';
comment on column coupon_user.used_at is '使用时间';
comment on column coupon_user.created_at is '创建时间';

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"
"errors"
"log/slog"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
@@ -50,9 +49,8 @@ func authUser(loginType PwdLoginType, username, password string) (user *m.User,
}
if user == nil {
user = &m.User{
Phone: username,
Username: u.P(username),
Status: m.UserStatusEnabled,
Phone: username,
Status: m.UserStatusEnabled,
}
}
case PwdLoginByEmail:
@@ -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) {
// 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code)
err := s.Verifier.VerifySms(context.Background(), username, code, s.VerifierSmsPurposeLogin)
if err != nil {
return nil, core.NewBizErr("短信认证失败", err)
}

View File

@@ -537,10 +537,10 @@ func introspectUser(ctx *fiber.Ctx, authCtx *AuthCtx) error {
// 掩码敏感信息
if profile.Phone != "" {
profile.Phone = maskPhone(profile.Phone)
profile.Phone = u.MaskPhone(profile.Phone)
}
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 {
@@ -579,20 +579,6 @@ func introspectAdmin(ctx *fiber.Ctx, authCtx *AuthCtx) error {
}{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 {
UserID int32 `json:"user_id"`
ClientID int32 `json:"client_id"`

View File

@@ -118,7 +118,7 @@ func Query(in any) url.Values {
case int:
out.Add(name, strconv.Itoa(value))
case bool:
if tags[1] == "b2i" {
if len(tags) > 1 && tags[1] == "b2i" {
out.Add(name, u.Ternary(value, "1", "0"))
} else {
out.Add(name, strconv.FormatBool(value))

View File

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

View File

@@ -48,9 +48,15 @@ const (
ScopeUserWriteBalanceDec = string("user:write:balance:dec") // 减少用户余额
ScopeUserWriteBind = string("user:write:bind") // 用户认领
ScopeCoupon = string("coupon") // 优惠券
ScopeCouponRead = string("coupon:read") // 读取优惠券列表
ScopeCouponWrite = string("coupon:write") // 写入优惠券
ScopeCoupon = string("coupon") // 优惠券
ScopeCouponRead = string("coupon:read") // 读取优惠券列表
ScopeCouponWrite = string("coupon:write") // 写入优惠券
ScopeCouponWriteAssign = string("coupon:write:assign") // 发放优惠券
ScopeCouponUser = string("coupon_user") // 用户优惠券
ScopeCouponUserRead = string("coupon_user:read") // 读取用户优惠券列表
ScopeCouponUserReadOfUser = string("coupon_user:read:of_user") // 读取指定用户的用户优惠券列表
ScopeCouponUserWrite = string("coupon_user:write") // 写入用户优惠券
ScopeBatch = string("batch") // 批次
ScopeBatchRead = string("batch:read") // 读取批次列表
@@ -62,6 +68,11 @@ const (
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 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") // 交易
ScopeTradeRead = string("trade:read") // 读取交易列表
ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表

View File

@@ -79,7 +79,6 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
slog.Warn("未处理的异常", slog.String("type", t.String()), slog.String("error", err.Error()))
}
slog.Warn(message)
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(code).SendString(message)
}

View File

@@ -9,3 +9,9 @@ const RemoveChannel = "channel:remove"
func NewRemoveChannel(batch string) *asynq.Task {
return asynq.NewTask(RemoveChannel, []byte(batch))
}
const ClearExpiredChannels = "channel:clear_expired"
func NewClearExpiredChannels() *asynq.Task {
return asynq.NewTask(ClearExpiredChannels, nil)
}

View File

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

View File

@@ -11,6 +11,65 @@ import (
"github.com/gofiber/fiber/v2"
)
// PageBalanceActivity 分页查询当前用户的余额变动记录
func PageBalanceActivity(c *fiber.Ctx) error {
// 获取当前用户ID
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 解析请求参数
req := new(PageBalanceActivityByUserReq)
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 构造查询条件
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(authCtx.User.ID))
if req.BillNo != nil {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
}
if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
}
if req.CreatedAtEnd != nil {
t := u.DateTail(*req.CreatedAtEnd)
do = do.Where(q.BalanceActivity.CreatedAt.Lte(t))
}
// 查询余额变动列表
list, total, err := q.BalanceActivity.
Joins(q.BalanceActivity.Bill).
Select(
q.BalanceActivity.ALL,
q.Bill.As("Bill").ID.As("Bill__id"),
q.Bill.As("Bill").BillNo.As("Bill__bill_no"),
).
Where(do).
Order(q.BalanceActivity.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageBalanceActivityByUserReq struct {
core.PageReq
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
}
// PageBalanceActivityByAdmin 分页查询所有余额变动记录
func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
// 检查权限
@@ -30,6 +89,9 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
if req.UserPhone != nil {
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
}
if req.BillNo != nil {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
}
if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
@@ -68,6 +130,7 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
type PageBalanceActivityByAdminReq struct {
core.PageReq
UserPhone *string `json:"user_phone,omitempty"`
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
}
@@ -88,6 +151,9 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
// 构造查询条件
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 {
t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
@@ -124,6 +190,7 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
type PageBalanceActivityOfUserByAdminReq struct {
core.PageReq
UserID int32 `json:"user_id" validate:"required"`
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
}

View File

@@ -172,12 +172,7 @@ type ListChannelsReq struct {
// CreateChannel 创建新通道
func CreateChannel(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 不检查权限,允许 api 调用
// 解析参数
req := new(CreateChannelReq)
@@ -201,7 +196,7 @@ func CreateChannel(c *fiber.Ctx) error {
req.AuthType == s.ChannelAuthTypeIp,
req.AuthType == s.ChannelAuthTypePass,
req.Count,
s.EdgeFilter{
&s.EdgeFilter{
Isp: isp,
Prov: req.Prov,
City: req.City,
@@ -217,6 +212,7 @@ func CreateChannel(c *fiber.Ctx) error {
resp[i] = &CreateChannelRespItem{
Proto: req.Protocol,
Host: channel.Host,
IP: channel.Proxy.IP.String(),
Port: channel.Port,
}
if req.AuthType == s.ChannelAuthTypePass {
@@ -240,6 +236,7 @@ type CreateChannelReq struct {
type CreateChannelRespItem struct {
Proto int `json:"-"`
Host string `json:"host"`
IP string `json:"ip"`
Port uint16 `json:"port"`
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`

View File

@@ -103,3 +103,27 @@ func DeleteCoupon(c *fiber.Ctx) error {
return nil
}
func AssignCoupon(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponWriteAssign)
if err != nil {
return err
}
var req AssignCouponReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
err = s.Coupon.Assign(req.CouponID, req.UserID)
if err != nil {
return err
}
return nil
}
type AssignCouponReq struct {
CouponID int32 `json:"coupon_id" validate:"required"`
UserID int32 `json:"user_id" validate:"required"`
}

330
web/handlers/coupon_user.go Normal file
View File

@@ -0,0 +1,330 @@
package handlers
import (
"errors"
"platform/pkg/u"
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/gorm"
)
// PageCouponUser 分页查询当前用户已发放优惠券
func PageCouponUser(c *fiber.Ctx) error {
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
var req PageCouponUserReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
conds := couponUserPageConditions(req.CouponUserPageFilter)
conds = append(conds, q.CouponUser.UserID.Eq(authCtx.User.ID))
list, total, err := q.CouponUser.
Joins(q.CouponUser.Coupon).
Select(couponUserSelect(false)...).
Where(conds...).
Order(q.CouponUser.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageCouponUserReq struct {
core.PageReq
CouponUserPageFilter
}
// GetCouponUser 获取当前用户已发放优惠券详情
func GetCouponUser(c *fiber.Ctx) error {
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
item, err := q.CouponUser.
Joins(q.CouponUser.Coupon).
Select(couponUserSelect(false)...).
Where(
q.CouponUser.ID.Eq(req.Id),
q.CouponUser.UserID.Eq(authCtx.User.ID),
).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(item)
}
// PageCouponUserByAdmin 分页查询全部已发放优惠券
func PageCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead)
if err != nil {
return err
}
var req PageCouponUserByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
conds := couponUserPageConditions(req.CouponUserPageFilter)
if req.UserID != nil {
conds = append(conds, q.CouponUser.UserID.Eq(*req.UserID))
}
if req.UserPhone != nil {
conds = append(conds, q.User.As("User").Phone.Eq(*req.UserPhone))
}
list, total, err := q.CouponUser.
Joins(q.CouponUser.Coupon, q.CouponUser.User).
Select(couponUserSelect(true)...).
Where(conds...).
Order(q.CouponUser.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageCouponUserByAdminReq struct {
core.PageReq
CouponUserPageFilter
UserID *int32 `json:"user_id,omitempty"`
UserPhone *string `json:"user_phone,omitempty"`
}
// PageCouponUserOfUserByAdmin 分页查询指定用户已发放优惠券
func PageCouponUserOfUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserReadOfUser)
if err != nil {
return err
}
var req PageCouponUserOfUserByAdminReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
conds := couponUserPageConditions(req.CouponUserPageFilter)
conds = append(conds, q.CouponUser.UserID.Eq(req.UserID))
list, total, err := q.CouponUser.
Joins(q.CouponUser.Coupon, q.CouponUser.User).
Select(couponUserSelect(true)...).
Where(conds...).
Order(q.CouponUser.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageCouponUserOfUserByAdminReq struct {
core.PageReq
CouponUserPageFilter
UserID int32 `json:"user_id" validate:"required"`
}
// GetCouponUserByAdmin 获取已发放优惠券详情
func GetCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserRead)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
item, err := q.CouponUser.
Joins(q.CouponUser.Coupon, q.CouponUser.User).
Select(couponUserSelect(true)...).
Where(q.CouponUser.ID.Eq(req.Id)).
Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
return c.JSON(item)
}
func CreateCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
if err != nil {
return err
}
var req s.CreateCouponUserData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.CouponUser.Create(req); err != nil {
return err
}
return nil
}
func UpdateCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
if err != nil {
return err
}
var req s.UpdateCouponUserData
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.CouponUser.Update(req); err != nil {
return err
}
return nil
}
func DeleteCouponUserByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeCouponUserWrite)
if err != nil {
return err
}
var req core.IdReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
if err := s.CouponUser.Delete(req.Id); err != nil {
return err
}
return nil
}
type CouponUserPageFilter struct {
CouponID *int32 `json:"coupon_id,omitempty"`
CouponName *string `json:"coupon_name,omitempty"`
Status *m.CouponUserStatus `json:"status,omitempty"`
Expired *bool `json:"expired,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
ExpireAtStart *time.Time `json:"expire_at_start,omitempty"`
ExpireAtEnd *time.Time `json:"expire_at_end,omitempty"`
UsedAtStart *time.Time `json:"used_at_start,omitempty"`
UsedAtEnd *time.Time `json:"used_at_end,omitempty"`
}
func couponUserPageConditions(req CouponUserPageFilter) []gen.Condition {
conds := make([]gen.Condition, 0)
if req.CouponID != nil {
conds = append(conds, q.CouponUser.CouponID.Eq(*req.CouponID))
}
if req.CouponName != nil {
conds = append(conds, q.Coupon.As("Coupon").Name.Like("%"+*req.CouponName+"%"))
}
if req.Status != nil {
conds = append(conds, q.CouponUser.Status.Eq(int(*req.Status)))
}
if req.Expired != nil {
if *req.Expired {
conds = append(conds, q.CouponUser.ExpireAt.IsNotNull(), q.CouponUser.ExpireAt.Lte(time.Now()))
} else {
conds = append(conds, q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now())))
}
}
if req.CreatedAtStart != nil {
conds = append(conds, q.CouponUser.CreatedAt.Gte(u.DateHead(*req.CreatedAtStart)))
}
if req.CreatedAtEnd != nil {
conds = append(conds, q.CouponUser.CreatedAt.Lte(u.DateTail(*req.CreatedAtEnd)))
}
if req.ExpireAtStart != nil {
conds = append(conds, q.CouponUser.ExpireAt.Gte(u.DateHead(*req.ExpireAtStart)))
}
if req.ExpireAtEnd != nil {
conds = append(conds, q.CouponUser.ExpireAt.Lte(u.DateTail(*req.ExpireAtEnd)))
}
if req.UsedAtStart != nil {
conds = append(conds, q.CouponUser.UsedAt.Gte(u.DateHead(*req.UsedAtStart)))
}
if req.UsedAtEnd != nil {
conds = append(conds, q.CouponUser.UsedAt.Lte(u.DateTail(*req.UsedAtEnd)))
}
return conds
}
func couponUserSelect(includeUser bool) []field.Expr {
cols := []field.Expr{
q.CouponUser.ALL,
q.Coupon.As("Coupon").ID.As("Coupon__id"),
q.Coupon.As("Coupon").Name.As("Coupon__name"),
q.Coupon.As("Coupon").Amount.As("Coupon__amount"),
q.Coupon.As("Coupon").MinAmount.As("Coupon__min_amount"),
q.Coupon.As("Coupon").Count_.As("Coupon__count"),
q.Coupon.As("Coupon").Status.As("Coupon__status"),
q.Coupon.As("Coupon").ExpireType.As("Coupon__expire_type"),
q.Coupon.As("Coupon").ExpireAt.As("Coupon__expire_at"),
q.Coupon.As("Coupon").ExpireIn.As("Coupon__expire_in"),
q.Coupon.As("Coupon").CreatedAt.As("Coupon__created_at"),
q.Coupon.As("Coupon").UpdatedAt.As("Coupon__updated_at"),
}
if includeUser {
cols = append(cols,
q.User.As("User").ID.As("User__id"),
q.User.As("User").Phone.As("User__phone"),
q.User.As("User").Name.As("User__name"),
)
}
return cols
}

View File

@@ -34,6 +34,15 @@ func AllProductByAdmin(c *fiber.Ctx) error {
type AllProductsByAdminReq struct {
}
func AllProduct(c *fiber.Ctx) error {
infos, err := s.Product.AllProductSaleInfos()
if err != nil {
return err
}
return c.JSON(infos)
}
func CreateProduct(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite)
if err != nil {

View File

@@ -1,61 +1,123 @@
package handlers
import (
"net/netip"
"platform/pkg/env"
"platform/web/auth"
"platform/web/core"
"platform/web/globals"
g "platform/web/globals"
s "platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
)
func DebugRegisterProxyBaiYin(c *fiber.Ctx) error {
if env.RunMode != env.RunModeDev {
return fiber.ErrNotFound
}
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()
func PageProxyByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
if err != nil {
return err
}
req := new(RegisterProxyBaiyinReq)
err = globals.Validator.ParseBody(c, req)
var req core.PageReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
list, total, err := s.Proxy.Page(req)
if err != nil {
return err
}
addr, err := netip.ParseAddr(req.IP)
if err != nil {
return core.NewServErr("IP地址格式错误", err)
}
err = s.Proxy.RegisterBaiyin(req.Name, addr, req.Username, req.Password)
if err != nil {
return core.NewServErr("注册失败", err)
}
return nil
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type RegisterProxyBaiyinReq struct {
Name string `json:"name" validate:"required"`
IP string `json:"ip" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
func AllProxyByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProxyRead)
if err != nil {
return err
}
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 报告上线

View File

@@ -84,6 +84,7 @@ func PageResourceShort(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset())
} else {
total, err = q.Resource.
Joins(q.Resource.Short).
Where(do).
Count()
if err != nil {
@@ -180,6 +181,7 @@ func PageResourceLong(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset())
} else {
total, err = q.Resource.
Joins(q.Resource.Long).
Where(do).
Count()
if err != nil {
@@ -609,6 +611,30 @@ func UpdateResourceByAdmin(c *fiber.Ctx) error {
return c.JSON(nil)
}
func UpdateResourceCheckIP(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
var req struct {
core.IdReq
CheckIP bool `json:"checkip"`
}
if err := c.BodyParser(&req); err != nil {
return err
}
if err := s.Resource.Update(&s.UpdateResourceData{
IdReq: req.IdReq,
CheckIP: &req.CheckIP,
}); err != nil {
return err
}
return c.JSON(nil)
}
// StatisticResourceFree 统计每日可用
func StatisticResourceFree(c *fiber.Ctx) error {
// 检查权限
@@ -797,10 +823,7 @@ type CreateResourceReq struct {
// ResourcePrice 套餐价格
func ResourcePrice(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitSecretClient()
if err != nil {
return err
}
ac := auth.GetAuthCtx(c)
// 解析请求参数
var req = new(CreateResourceReq)
@@ -809,7 +832,7 @@ func ResourcePrice(c *fiber.Ctx) error {
}
// 获取套餐价格
detail, err := req.TradeDetail(nil)
detail, err := req.TradeDetail(ac.User)
if err != nil {
return err
}
@@ -817,11 +840,13 @@ func ResourcePrice(c *fiber.Ctx) error {
// 计算折扣
return c.JSON(ResourcePriceResp{
Price: detail.Amount.StringFixed(2),
Discounted: detail.Actual.StringFixed(2),
Discounted: detail.Discounted.StringFixed(2),
Actual: detail.Actual.StringFixed(2),
})
}
type ResourcePriceResp struct {
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 {
return err
}
if authCtx.User.IDType == m.UserIDTypeUnverified {
return core.NewBizErr("请先实名认证后再购买")
}
// 解析请求参数
req := new(TradeCreateReq)

View File

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

View File

@@ -5,6 +5,7 @@ import (
"platform/pkg/env"
"platform/web/auth"
"platform/web/services"
s "platform/web/services"
"regexp"
"strconv"
@@ -13,12 +14,11 @@ import (
)
type VerifierReq struct {
Purpose services.VerifierSmsPurpose `json:"purpose"`
Phone string `json:"phone"`
Purpose s.VerifierSmsPurpose `json:"purpose"`
Phone string `json:"phone"`
}
func SmsCode(c *fiber.Ctx) error {
func SendSmsCode(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil {
return err
@@ -38,9 +38,9 @@ func SmsCode(c *fiber.Ctx) error {
}
// 发送身份验证码
err = services.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
err = s.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
if err != nil {
var sErr services.VerifierServiceSendLimitErr
var sErr s.VerifierServiceSendLimitErr
if errors.As(err, &sErr) {
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
}
@@ -51,6 +51,23 @@ func SmsCode(c *fiber.Ctx) error {
return nil
}
func SendSmsCodeForPassword(c *fiber.Ctx) error {
ac, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
if err := s.Verifier.SendSms(c.Context(), ac.User.Phone, s.VerifierSmsPurposePassword); err != nil {
var sErr s.VerifierServiceSendLimitErr
if errors.As(err, &sErr) {
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
}
return err
}
return nil
}
func DebugGetSmsCode(c *fiber.Ctx) error {
if env.RunMode != env.RunModeDev {
return fiber.NewError(fiber.StatusForbidden, "not allowed")

View File

@@ -2,6 +2,7 @@ package handlers
import (
"errors"
"fmt"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/auth"
@@ -97,13 +98,31 @@ func CreateWhitelist(c *fiber.Ctx) error {
}
// 创建白名单
err = q.Whitelist.Create(&m.Whitelist{
UserID: authCtx.User.ID,
IP: u.Z(ip),
Remark: &req.Remark,
uid := authCtx.User.ID
err = g.Redsync.WithLock(whitelistKey(uid), func() error {
count, err := q.Whitelist.Where(
q.Whitelist.UserID.Eq(uid),
).Count()
if err != nil {
return core.NewServErr("获取白名单数量失败", err)
}
if count >= 5 {
return core.NewBizErr("白名单数量已达上限")
}
err = q.Whitelist.Create(&m.Whitelist{
UserID: authCtx.User.ID,
IP: u.Z(ip),
Remark: &req.Remark,
})
if err != nil {
return core.NewServErr("添加白名单失败", err)
}
return nil
})
if err != nil {
return core.NewServErr("添加白名单失败", err)
return err
}
return nil
@@ -206,3 +225,7 @@ func secureAddr(str string) (*orm.Inet, error) {
}
return ip, nil
}
func whitelistKey(userID int32) string {
return fmt.Sprintf("platform:whitelist:add:%d", userID)
}

View File

@@ -4,13 +4,13 @@ import "time"
// CouponUser 优惠券发放表
type CouponUser struct {
ID int32 `json:"id" gorm:"column:id;primaryKey"` // 记录ID
CouponID int32 `json:"coupon_id" gorm:"column:coupon_id"` // 优惠券ID
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
Status CouponStatus `json:"status" gorm:"column:status"` // 使用状态0-未使用1-已使用
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
UsedAt *time.Time `json:"used_at,omitempty" gorm:"column:used_at"` // 使用时间
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
ID int32 `json:"id" gorm:"column:id;primaryKey"` // 记录ID
CouponID int32 `json:"coupon_id" gorm:"column:coupon_id"` // 优惠券ID
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
Status CouponUserStatus `json:"status" gorm:"column:status"` // 使用状态0-未使用1-已使用2-已禁用
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
UsedAt *time.Time `json:"used_at,omitempty" gorm:"column:used_at"` // 使用时间
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
Coupon *Coupon `json:"coupon,omitempty" gorm:"foreignKey:CouponID"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
@@ -20,6 +20,7 @@ type CouponUser struct {
type CouponUserStatus int
const (
CouponUserStatusUnused CouponUserStatus = 0 // 未使用
CouponUserStatusUsed CouponUserStatus = 1 // 已使用
CouponUserStatusUnused CouponUserStatus = 0 // 未使用
CouponUserStatusUsed CouponUserStatus = 1 // 已使用
CouponUserStatusDisabled CouponUserStatus = 2 // 已禁用
)

View File

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

View File

@@ -16,6 +16,8 @@ type ProductSku struct {
Price decimal.Decimal `json:"price" gorm:"column:price"` // 定价
PriceMin decimal.Decimal `json:"price_min" gorm:"column:price_min"` // 最低价格
Status SkuStatus `json:"status" gorm:"column:status"` // SKU 状态0-禁用1-正常
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
CountMin int32 `json:"count_min" gorm:"column:count_min"` // 最小购买数量
Product *Product `json:"product,omitempty" gorm:"foreignKey:ProductID"`
Discount *ProductDiscount `json:"discount,omitempty" gorm:"foreignKey:DiscountId"`

View File

@@ -12,6 +12,7 @@ type Resource struct {
Active bool `json:"active" gorm:"column:active"` // 套餐状态
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型1-短效动态2-长效动态
Code string `json:"code" gorm:"column:code"` // 产品编码
CheckIP bool `json:"checkip" gorm:"column:checkip"` // 是否检查IP
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
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
Product struct {
field.RelationField
Skus struct {
field.RelationField
}
}
Discount struct {
field.RelationField
@@ -173,6 +176,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
field.RelationField
Product struct {
field.RelationField
Skus struct {
field.RelationField
}
}
Discount struct {
field.RelationField
@@ -184,6 +190,9 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
field.RelationField
Product struct {
field.RelationField
Skus struct {
field.RelationField
}
}
Discount struct {
field.RelationField
@@ -192,8 +201,16 @@ func newBalanceActivity(db *gorm.DB, opts ...gen.DOOption) balanceActivity {
RelationField: field.NewRelation("Bill.Resource.Short.Sku", "models.ProductSku"),
Product: struct {
field.RelationField
Skus struct {
field.RelationField
}
}{
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 {
field.RelationField
@@ -482,6 +499,9 @@ type balanceActivityBelongsToBill struct {
field.RelationField
Product struct {
field.RelationField
Skus struct {
field.RelationField
}
}
Discount struct {
field.RelationField

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,29 @@ func newProduct(db *gorm.DB, opts ...gen.DOOption) product {
_product.Description = field.NewString(tableName, "description")
_product.Sort = field.NewInt32(tableName, "sort")
_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()
@@ -55,6 +78,7 @@ type product struct {
Description field.String
Sort field.Int32
Status field.Int
Skus productHasManySkus
fieldMap map[string]field.Expr
}
@@ -96,7 +120,7 @@ func (p *product) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
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["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt
@@ -106,18 +130,113 @@ func (p *product) fillFieldMap() {
p.fieldMap["description"] = p.Description
p.fieldMap["sort"] = p.Sort
p.fieldMap["status"] = p.Status
}
func (p product) clone(db *gorm.DB) product {
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
}
func (p product) replaceDB(db *gorm.DB) product {
p.productDo.ReplaceDB(db)
p.Skus.db = db.Session(&gorm.Session{})
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 }
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.PriceMin = field.NewField(tableName, "price_min")
_productSku.Status = field.NewInt32(tableName, "status")
_productSku.Sort = field.NewInt32(tableName, "sort")
_productSku.CountMin = field.NewInt32(tableName, "count_min")
_productSku.Product = productSkuBelongsToProduct{
db: db.Session(&gorm.Session{}),
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{
@@ -70,6 +93,8 @@ type productSku struct {
Price field.Field
PriceMin field.Field
Status field.Int32
Sort field.Int32
CountMin field.Int32
Product productSkuBelongsToProduct
Discount productSkuBelongsToDiscount
@@ -100,6 +125,8 @@ func (p *productSku) updateTableName(table string) *productSku {
p.Price = field.NewField(table, "price")
p.PriceMin = field.NewField(table, "price_min")
p.Status = field.NewInt32(table, "status")
p.Sort = field.NewInt32(table, "sort")
p.CountMin = field.NewInt32(table, "count_min")
p.fillFieldMap()
@@ -116,7 +143,7 @@ func (p *productSku) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
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["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt
@@ -128,6 +155,8 @@ func (p *productSku) fillFieldMap() {
p.fieldMap["price"] = p.Price
p.fieldMap["price_min"] = p.PriceMin
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
field.RelationField
Skus struct {
field.RelationField
Product struct {
field.RelationField
}
Discount struct {
field.RelationField
}
}
}
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"),
Product: struct {
field.RelationField
Skus struct {
field.RelationField
}
}{
RelationField: field.NewRelation("ProductSku.Product", "models.Product"),
Skus: struct {
field.RelationField
}{
RelationField: field.NewRelation("ProductSku.Product.Skus", "models.ProductSku"),
},
},
Discount: struct {
field.RelationField
@@ -331,6 +339,9 @@ type productSkuUserBelongsToProductSku struct {
Product struct {
field.RelationField
Skus struct {
field.RelationField
}
}
Discount struct {
field.RelationField

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import (
"platform/pkg/env"
auth2 "platform/web/auth"
"platform/web/handlers"
s "platform/web/services"
"time"
q "platform/web/queries"
@@ -25,7 +26,6 @@ func ApplyRouters(app *fiber.App) {
if env.RunMode == env.RunModeDev {
debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear)
debug.Get("/session/now", func(ctx *fiber.Ctx) error {
rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now())).Find()
@@ -34,6 +34,12 @@ func ApplyRouters(app *fiber.App) {
}
return ctx.JSON(rs)
})
debug.Get("/channel/clear-expired", func(ctx *fiber.Ctx) error {
if err := s.Channel.ClearExpiredChannels(); err != nil {
return err
}
return ctx.SendStatus(fiber.StatusOK)
})
}
}
@@ -67,6 +73,7 @@ func userRouter(api fiber.Router) {
resource.Post("/list/short", handlers.PageResourceShort)
resource.Post("/list/long", handlers.PageResourceLong)
resource.Post("/create", handlers.CreateResource)
resource.Post("/update/checkip", handlers.UpdateResourceCheckIP)
resource.Post("/statistics/free", handlers.StatisticResourceFree)
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
@@ -91,6 +98,15 @@ func userRouter(api fiber.Router) {
bill := api.Group("/bill")
bill.Post("/list", handlers.ListBill)
// 余额变动
balance := api.Group("/balance")
balance.Post("/page", handlers.PageBalanceActivity)
// 已发放优惠券
couponUser := api.Group("/coupon-user")
couponUser.Post("/page", handlers.PageCouponUser)
couponUser.Post("/get", handlers.GetCouponUser)
// 公告
announcement := api.Group("/announcement")
announcement.Post("/list", handlers.ListAnnouncements)
@@ -109,6 +125,14 @@ func userRouter(api fiber.Router) {
// 前台
inquiry := api.Group("/inquiry")
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 +140,7 @@ func clientRouter(api fiber.Router) {
client := api
// 验证短信令牌
client.Post("/verify/sms", handlers.SmsCode)
client.Post("/verify/sms", handlers.SendSmsCode)
// 套餐定价查询
resource := client.Group("/resource")
@@ -126,9 +150,6 @@ func clientRouter(api fiber.Router) {
channel := client.Group("/channel")
channel.Post("/remove", handlers.RemoveChannels)
// 代理网关注册
proxy := client.Group("/proxy")
proxy.Post("/register/baidyin", handlers.ProxyRegisterBaiYin)
}
// 管理员接口路由
@@ -188,6 +209,15 @@ func adminRouter(api fiber.Router) {
channel.Post("/page", handlers.PageChannelByAdmin)
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 交易
var trade = api.Group("/trade")
trade.Post("/page", handlers.PageTradeByAdmin)
@@ -235,4 +265,14 @@ func adminRouter(api fiber.Router) {
coupon.Post("/create", handlers.CreateCoupon)
coupon.Post("/update", handlers.UpdateCoupon)
coupon.Post("/remove", handlers.DeleteCoupon)
coupon.Post("/update/assign", handlers.AssignCoupon)
// coupon-user 已发放优惠券
var couponUser = api.Group("/coupon-user")
couponUser.Post("/page", handlers.PageCouponUserByAdmin)
couponUser.Post("/page/of-user", handlers.PageCouponUserOfUserByAdmin)
couponUser.Post("/get", handlers.GetCouponUserByAdmin)
couponUser.Post("/create", handlers.CreateCouponUserByAdmin)
couponUser.Post("/update", handlers.UpdateCouponUserByAdmin)
couponUser.Post("/remove", handlers.DeleteCouponUserByAdmin)
}

View File

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

View File

@@ -24,22 +24,27 @@ var Channel = &channelServer{
}
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
ClearExpiredChannels() error
}
type channelServer struct {
provider ChannelServiceProvider
}
func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter...)
func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter)
}
func (s *channelServer) RemoveChannels(batch string) error {
return s.provider.RemoveChannels(batch)
}
func (s *channelServer) ClearExpiredChannels() error {
return s.provider.ClearExpiredChannels()
}
// 授权方式
type ChannelAuthType int
@@ -83,10 +88,11 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
return nil, ErrResourceNotExist
}
var info = &ResourceView{
Id: resource.ID,
User: *resource.User,
Active: resource.Active,
Type: resource.Type,
Id: resource.ID,
User: *resource.User,
Active: resource.Active,
Type: resource.Type,
CheckIP: resource.CheckIP,
}
switch resource.Type {
@@ -94,7 +100,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
var sub = resource.Short
info.ShortId = &sub.ID
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.Quota = sub.Quota
info.Used = sub.Used
@@ -142,10 +148,11 @@ type ResourceView struct {
Daily int32
LastAt *time.Time
Today int // 今日用量
CheckIP bool
}
// 检查用户是否可提取
func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*ResourceView, []string, error) {
func ensure(now time.Time, source netip.Addr, resourceId int32, authWhitelist bool, count int) (*ResourceView, []string, error) {
if count > 400 {
return nil, nil, core.NewBizErr("单次最多提取 400 个")
}
@@ -170,6 +177,10 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
return nil, nil, err
}
if authWhitelist && len(whitelists) == 0 {
return nil, nil, core.NewBizErr("当前白名单为空,请先添加白名单")
}
ips := make([]string, len(whitelists))
pass := false
for i, item := range whitelists {
@@ -178,7 +189,7 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
pass = true
}
}
if !pass {
if resource.CheckIP && !pass {
return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
}
@@ -232,7 +243,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
// 缩容通道
func remChans(proxy int32) error {
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 {
return fmt.Errorf("缩容通道失败: %w", err)
}
@@ -268,11 +279,12 @@ func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
}
var RedisScriptLockChans = redis.NewScript(`
local free_key = KEYS[1]
local free_key = KEYS[1]
local batch_key = KEYS[2]
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
end
@@ -301,16 +313,17 @@ func freeChans(proxy int32, batch string) error {
}
var RedisScriptFreeChans = redis.NewScript(`
local free_key = KEYS[1]
local free_key = KEYS[1]
local batch_key = KEYS[2]
local chans = redis.call("LRANGE", batch_key, 0, -1)
redis.call("DEL", batch_key)
if redis.call("EXISTS", free_key) == 1 then
redis.call("SADD", free_key, unpack(chans))
if #chans == 0 then
return 1
end
redis.call("SADD", free_key, unpack(chans))
redis.call("DEL", batch_key)
return 1
`)

View File

@@ -17,23 +17,21 @@ import (
"time"
"github.com/hibiken/asynq"
"gorm.io/gen"
"gorm.io/gen/field"
)
type channelBaiyinProvider struct{}
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
var filter *EdgeFilter = nil
if len(edgeFilter) > 0 {
filter = &edgeFilter[0]
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) {
if filter == nil {
return nil, core.NewBizErr("缺少节点过滤条件")
}
now := time.Now()
batch := ID.GenReadable("bat")
// 检查并获取套餐与白名单
resource, whitelists, err := ensure(now, source, resourceId, count)
resource, whitelists, err := ensure(now, source, resourceId, authWhitelist, count)
if err != nil {
return nil, err
}
@@ -64,44 +62,57 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
}
proxy := proxyResult.Proxy
// 获取可用通道
chans, err := lockChans(proxy.ID, batch, count)
if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
}
// 取用端口
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 {
return err
}
if lockedProxy.Status != m.ProxyStatusOnline {
return core.NewBizErr("无可用主机,请稍后再试")
}
// 获取可用节点
edgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
Province: filter.Prov,
City: filter.City,
Isp: u.X(filter.Isp.String()),
Limit: &count,
NoRepeat: u.P(true),
NoDayRepeat: u.P(true),
ActiveTime: u.P(3600),
IpUnchangedTime: u.P(3600),
Sort: u.P("ip_unchanged_time_asc"),
chans, err = lockChans(proxy.ID, batch, count)
if err != nil {
return core.NewBizErr("无可用通道,请稍后再试", err)
}
proxy = *lockedProxy
return nil
})
if err != nil {
return nil, core.NewBizErr("获取可用节点失败", err)
return nil, err
}
if edgesResp.Total != count && len(edgesResp.Edges) != count {
return nil, core.NewBizErr("地区可用节点数量不足 [%s, %s] [%s]")
}
edges := edgesResp.Edges
// 准备通道数据
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batch),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("提交关闭通道任务失败", err)
}
// 取用节点
secret := strings.Split(u.Z(proxy.Secret), ":")
if len(secret) != 2 {
return nil, core.NewServErr(fmt.Sprintf("代理 %s 密钥格式错误", proxy.IP.String()), nil)
}
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
edges, err := getAvailableEdges(gateway, filter, count)
if err != nil {
return nil, err
}
// 绑定节点到端口
channels := make([]*m.Channel, count)
chanConfigs := make([]*g.PortConfigsReq, count)
edgeConfigs := make([]string, count)
edgeConfigs := make([]string, 0, count)
for i := range count {
ch := chans[i]
edge := edges[i]
if err != nil {
return nil, core.NewBizErr("解析通道地址失败", err)
}
// 通道数据
channels[i] = &m.Channel{
UserID: user.ID,
@@ -115,6 +126,7 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
FilterProv: filter.Prov,
FilterCity: filter.City,
ExpiredAt: expire,
Proxy: &proxy,
}
// 通道配置数据
@@ -139,29 +151,18 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
}
// 连接配置数据
edgeConfigs[i] = edge.EdgeID
}
// 提交异步任务关闭通道
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batch),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("提交关闭通道任务失败", err)
if edge.Type == EdgeInfoCloud {
edgeConfigs = append(edgeConfigs, edge.EdgeID)
}
}
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
var rs gen.ResultInfo
// 根据套餐类型和模式更新使用记录
isShortType := resource.Type == m.ResourceTypeShort
isLongType := resource.Type == m.ResourceTypeLong
switch {
case isShortType:
rs, err = q.ResourceShort.
// 更新使用记录
var err error
switch resource.Type {
case m.ResourceTypeShort:
_, err = q.ResourceShort.
Where(
q.ResourceShort.ID.Eq(*resource.ShortId),
q.ResourceShort.Used.Eq(resource.Used),
@@ -173,8 +174,8 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
q.ResourceShort.LastAt.Value(now),
)
case isLongType:
rs, err = q.ResourceLong.
case m.ResourceTypeLong:
_, err = q.ResourceLong.
Where(
q.ResourceLong.ID.Eq(*resource.LongId),
q.ResourceLong.Used.Eq(resource.Used),
@@ -192,9 +193,6 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
if rs.RowsAffected == 0 {
return core.NewServErr("套餐使用记录不存在")
}
// 保存通道
err = q.Channel.
@@ -227,8 +225,6 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
}
// 提交配置
secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
if env.RunMode == env.RunModeProd {
// 连接节点到网关
@@ -243,10 +239,11 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
// 启用网关代理通道
err = gateway.GatewayPortConfigs(chanConfigs)
if err != nil {
slog.Warn("提交代理端口配置失败", "error", err.Error())
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
}
} else {
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String())
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "count", len(chanConfigs), "remote", len(edgeConfigs))
for _, item := range chanConfigs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
@@ -294,21 +291,25 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
// 提交配置
if env.RunMode == env.RunModeProd {
// 断开节点连接
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
// 清空通道配置
secret := strings.Split(*proxy.Secret, ":")
secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
err := gateway.GatewayPortConfigs(configs)
if err != nil {
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
}
// 断开节点连接
_, err = g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
if err != nil {
slog.Warn("断开云平台连接失败", "error", err.Error())
return core.NewServErr("断开云平台连接失败", err)
}
} else {
slog.Debug("清除代理端口配置", "proxy", proxy.IP)
for _, item := range configs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
@@ -321,6 +322,91 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
return err
}
slog.Debug("清除代理端口配置", "time", time.Since(start).String())
slog.Debug("清除代理端口配置", "proxy", proxy.IP.String(), "batch", batch, "duration", time.Since(start).String())
return nil
}
func (s *channelBaiyinProvider) ClearExpiredChannels() error {
now := time.Now().Add(time.Hour)
var batches []struct{ BatchNo string }
err := q.Channel.Select(q.Channel.BatchNo).Where(q.Channel.ExpiredAt.Lt(now)).Group(q.Channel.BatchNo).Scan(&batches)
if err != nil {
return core.NewServErr("查询过期通道失败", err)
}
for _, batch := range batches {
err := s.RemoveChannels(batch.BatchNo)
if err != nil {
slog.Error("清理过期通道失败", "batch", batch.BatchNo, "error", err)
}
}
return nil
}
func getAvailableEdges(gateway g.GatewayClient, filter *EdgeFilter, count int) ([]EdgeInfo, error) {
edges := make([]EdgeInfo, 0, count)
// 先查本地
localEdgesResp, err := gateway.GatewayEdge(&g.GatewayEdgeReq{
Province: filter.Prov,
City: filter.City,
Isp: u.X(filter.Isp.String()),
Limit: &count,
Assigned: u.P(false),
})
if err != nil {
return nil, core.NewBizErr("获取可用节点失败[1]", err)
}
for id, _ := range localEdgesResp {
edges = append(edges, EdgeInfo{
Type: EdgeInfoLocal,
EdgeID: id,
})
}
if len(edges) >= count {
return edges, nil
}
// 再查云端无重复
remaining := count - len(edges)
cloudEdgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
Province: filter.Prov,
City: filter.City,
Isp: u.X(filter.Isp.String()),
Limit: &remaining,
NoRepeat: u.P(true),
ActiveTime: u.P(3600),
IpUnchangedTime: u.P(3600),
})
if err != nil {
return nil, core.NewBizErr("获取可用节点失败[2]", err)
}
for _, edge := range cloudEdgesResp.Edges {
edges = append(edges, EdgeInfo{
Type: EdgeInfoCloud,
EdgeID: edge.EdgeID,
})
}
if len(edges) >= count {
return edges, nil
}
// 不能和已有的重复,如果有重复则再次查询云端补足,二次提取还有重复则放弃
return nil, core.NewBizErr("地区可用节点数量不足")
}
type EdgeInfo struct {
Type EdgeInfoType
EdgeID string
}
type EdgeInfoType string
const (
EdgeInfoLocal EdgeInfoType = "local"
EdgeInfoCloud EdgeInfoType = "cloud"
)

View File

@@ -68,14 +68,24 @@ func (s *couponService) Update(data UpdateCouponData) error {
do = append(do, q.Coupon.Status.Value(int(*data.Status)))
}
if data.ExpireType != nil {
switch *data.ExpireType {
case m.CouponExpireTypeNever:
do = append(do, q.Coupon.ExpireAt.Null(), q.Coupon.ExpireIn.Null())
case m.CouponExpireTypeFixed:
if data.ExpireAt == nil {
return core.NewBizErr("expire_at 不能为空")
}
do = append(do, q.Coupon.ExpireAt.Value(*data.ExpireAt), q.Coupon.ExpireIn.Null())
case m.CouponExpireTypeRelative:
if data.ExpireIn == nil {
return core.NewBizErr("expire_in 不能为空")
}
do = append(do, q.Coupon.ExpireAt.Null(), q.Coupon.ExpireIn.Value(*data.ExpireIn))
}
do = append(do, q.Coupon.ExpireType.Value(int(*data.ExpireType)))
}
if data.ExpireAt != nil {
do = append(do, q.Coupon.ExpireAt.Value(*data.ExpireAt))
}
if data.ExpireIn != nil {
do = append(do, q.Coupon.ExpireIn.Value(*data.ExpireIn))
}
_, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...)
return err
@@ -98,6 +108,13 @@ func (s *couponService) Delete(id int32) error {
return err
}
func (s *couponService) Assign(couponID int32, userID int32) error {
return CouponUser.Create(CreateCouponUserData{
CouponID: couponID,
UserID: userID,
})
}
// GetUserCoupon 获取用户的指定优惠券
func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Decimal) (*m.CouponUser, error) {
// 获取优惠券
@@ -105,7 +122,7 @@ func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Deci
q.CouponUser.ID.Eq(cuid),
q.CouponUser.UserID.Eq(uid),
q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)),
q.CouponUser.ExpireAt.Gt(time.Now()),
q.CouponUser.Where(q.CouponUser.ExpireAt.IsNull()).Or(q.CouponUser.ExpireAt.Gt(time.Now())),
).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("优惠券不存在或已失效")

255
web/services/coupon_user.go Normal file
View File

@@ -0,0 +1,255 @@
package services
import (
"errors"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
"time"
"gorm.io/gen/field"
"gorm.io/gorm"
)
var CouponUser = &couponUserService{}
type couponUserService struct{}
func (s *couponUserService) Create(data CreateCouponUserData) error {
now := time.Now()
status := u.Else(data.Status, m.CouponUserStatusUnused)
if err := validateCouponUserStatus(status); err != nil {
return err
}
return q.Q.Transaction(func(tx *q.Query) error {
coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(data.CouponID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("优惠券不存在")
}
if err != nil {
return core.NewServErr("获取优惠券数据失败", err)
}
if coupon.Status != m.CouponStatusEnabled {
return core.NewBizErr("优惠券不可用")
}
if coupon.Count <= 0 {
return core.NewBizErr("优惠券已发放完")
}
_, err = tx.User.Where(tx.User.ID.Eq(data.UserID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("用户不存在")
}
if err != nil {
return core.NewServErr("获取用户数据失败", err)
}
expireAt := data.ExpireAt
if expireAt == nil {
expireAt = couponUserExpireAt(coupon, now)
}
usedAt := data.UsedAt
if status == m.CouponUserStatusUsed && usedAt == nil {
usedAt = &now
}
if status == m.CouponUserStatusUnused {
usedAt = nil
}
err = tx.CouponUser.Create(&m.CouponUser{
UserID: data.UserID,
CouponID: data.CouponID,
Status: status,
ExpireAt: expireAt,
UsedAt: usedAt,
})
if err != nil {
return core.NewServErr("发放优惠券失败", err)
}
return adjustCouponCount(tx, coupon.ID, -1)
})
}
type CreateCouponUserData struct {
CouponID int32 `json:"coupon_id" validate:"required"`
UserID int32 `json:"user_id" validate:"required"`
Status *m.CouponUserStatus `json:"status"`
ExpireAt *time.Time `json:"expire_at"`
UsedAt *time.Time `json:"used_at"`
}
func (s *couponUserService) Update(data UpdateCouponUserData) error {
return q.Q.Transaction(func(tx *q.Query) error {
current, err := tx.CouponUser.Where(tx.CouponUser.ID.Eq(data.ID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewServErr("获取已发放优惠券失败", err)
}
do := make([]field.AssignExpr, 0)
if data.ExpireAtClear != nil && *data.ExpireAtClear {
do = append(do, tx.CouponUser.ExpireAt.Null())
} else if data.ExpireAt != nil {
do = append(do, tx.CouponUser.ExpireAt.Value(*data.ExpireAt))
}
if data.UsedAtClear != nil && *data.UsedAtClear {
do = append(do, tx.CouponUser.UsedAt.Null())
} else if data.UsedAt != nil {
do = append(do, tx.CouponUser.UsedAt.Value(*data.UsedAt))
}
if data.Status != nil {
if err := validateCouponUserStatus(*data.Status); err != nil {
return err
}
if current.Status != *data.Status {
if current.Status == m.CouponUserStatusUsed {
return core.NewBizErr("已使用的优惠券不能修改状态")
}
if current.Status == m.CouponUserStatusDisabled && *data.Status == m.CouponUserStatusUsed {
return core.NewBizErr("已禁用的优惠券不能标记为已使用")
}
switch *data.Status {
case m.CouponUserStatusUnused:
if current.Status == m.CouponUserStatusDisabled {
if err := adjustCouponCount(tx, current.CouponID, -1); err != nil {
return err
}
}
if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) {
do = append(do, tx.CouponUser.UsedAt.Null())
}
case m.CouponUserStatusUsed:
if data.UsedAt == nil && (data.UsedAtClear == nil || !*data.UsedAtClear) {
do = append(do, tx.CouponUser.UsedAt.Value(time.Now()))
}
case m.CouponUserStatusDisabled:
if current.Status == m.CouponUserStatusUnused {
if err := adjustCouponCount(tx, current.CouponID, 1); err != nil {
return err
}
}
}
do = append(do, tx.CouponUser.Status.Value(int(*data.Status)))
}
}
if len(do) == 0 {
return nil
}
result, err := tx.CouponUser.
Where(
tx.CouponUser.ID.Eq(data.ID),
tx.CouponUser.Status.Eq(int(current.Status)),
).
UpdateSimple(do...)
if err != nil {
return core.NewServErr("更新已发放优惠券失败", err)
}
if result.RowsAffected == 0 {
return core.NewBizErr("已发放优惠券状态已变化,请重试")
}
return nil
})
}
type UpdateCouponUserData struct {
ID int32 `json:"id" validate:"required"`
Status *m.CouponUserStatus `json:"status"`
ExpireAt *time.Time `json:"expire_at"`
ExpireAtClear *bool `json:"expire_at_clear"`
UsedAt *time.Time `json:"used_at"`
UsedAtClear *bool `json:"used_at_clear"`
}
func (s *couponUserService) Delete(id int32) error {
status := m.CouponUserStatusDisabled
return s.Update(UpdateCouponUserData{
ID: id,
Status: &status,
})
}
func (s *couponUserService) DeleteOfUser(id int32, userID int32) error {
assigned, err := q.CouponUser.Where(
q.CouponUser.ID.Eq(id),
q.CouponUser.UserID.Eq(userID),
).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("已发放优惠券不存在")
}
if err != nil {
return core.NewServErr("获取已发放优惠券失败", err)
}
if assigned.Status != m.CouponUserStatusUnused {
return core.NewBizErr("只能撤销未使用的优惠券")
}
return s.Delete(id)
}
func couponUserExpireAt(coupon *m.Coupon, now time.Time) *time.Time {
if coupon == nil {
return nil
}
switch coupon.ExpireType {
case m.CouponExpireTypeFixed:
return coupon.ExpireAt
case m.CouponExpireTypeRelative:
if coupon.ExpireIn == nil {
return nil
}
expireAt := now.Add(time.Duration(*coupon.ExpireIn) * 24 * time.Hour)
return &expireAt
default:
return nil
}
}
func validateCouponUserStatus(status m.CouponUserStatus) error {
switch status {
case m.CouponUserStatusUnused, m.CouponUserStatusUsed, m.CouponUserStatusDisabled:
return nil
default:
return core.NewBizErr("优惠券发放状态无效")
}
}
func adjustCouponCount(tx *q.Query, couponID int32, delta int32) error {
coupon, err := tx.Coupon.Where(tx.Coupon.ID.Eq(couponID)).Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return core.NewBizErr("优惠券不存在")
}
if err != nil {
return core.NewServErr("获取优惠券数据失败", err)
}
next := coupon.Count + delta
if next < 0 {
return core.NewBizErr("优惠券已发放完")
}
result, err := tx.Coupon.
Where(tx.Coupon.ID.Eq(couponID), tx.Coupon.Count_.Eq(coupon.Count)).
UpdateSimple(tx.Coupon.Count_.Value(next))
if err != nil {
return core.NewServErr("更新优惠券数量失败", err)
}
if result.RowsAffected == 0 {
return core.NewBizErr("优惠券库存已变化,请重试")
}
return nil
}

View File

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

View File

@@ -1,65 +1,216 @@
package services
import (
"context"
"fmt"
"net/netip"
"platform/pkg/u"
"platform/web/core"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"strconv"
"time"
"gorm.io/gen/field"
)
var Proxy = &proxyService{}
type proxyService struct{}
// AllProxies 获取所有代理
func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Proxy, error) {
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
func proxyStatusLockKey(id int32) string {
return fmt.Sprintf("platform:proxy:status:%d", id)
}
// RegisterBaiyin 注册新代理服务
func (s *proxyService) RegisterBaiyin(Name string, IP netip.Addr, username, password string) error {
// 保存代理信息
proxy := &m.Proxy{
Version: 0,
Mac: Name,
IP: orm.Inet{Addr: IP},
Secret: u.P(fmt.Sprintf("%s:%s", username, password)),
Type: m.ProxyTypeBaiYin,
Status: m.ProxyStatusOnline,
func hasUsedChans(proxyID int32) (bool, error) {
ctx := context.Background()
pattern := usedChansKey + ":" + strconv.Itoa(int(proxyID)) + ":*"
keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result()
if err != nil {
return false, err
}
if err := q.Proxy.Create(proxy); err != nil {
return core.NewServErr("保存通道数据失败")
return len(keys) > 0, nil
}
func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
if err := remChans(proxyID); err != nil {
return err
}
// 添加可用通道到 redis
chans := make([]netip.AddrPort, 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 {
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
}
// UnregisterBaiyin 注销代理服务
func (s *proxyService) UnregisterBaiyin(id int) error {
func (s *proxyService) Remove(id int32) 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
}
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 (
"fmt"
"log/slog"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
@@ -62,9 +63,10 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
var resource = m.Resource{
UserID: uid,
ResourceNo: u.P(ID.GenReadable("res")),
Active: true,
Type: data.Type,
Code: data.Type.Code(),
Active: true,
CheckIP: true,
}
switch data.Type {
@@ -120,14 +122,13 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
}
func (s *resourceService) Update(data *UpdateResourceData) error {
if data.Active == nil {
return core.NewBizErr("更新套餐失败active 不能为空")
}
do := make([]field.AssignExpr, 0)
if data.Active != nil {
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.
Where(q.Resource.ID.Eq(data.Id)).
@@ -141,38 +142,43 @@ func (s *resourceService) Update(data *UpdateResourceData) error {
type UpdateResourceData struct {
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.
Joins(q.ProductSku.Discount).
Where(q.ProductSku.Code.Eq(skuCode), q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled))).
Take()
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
amount := price.Mul(decimal.NewFromInt32(count))
amountMin := sku.PriceMin.Mul(decimal.NewFromInt32(count))
amount := sku.Price.Mul(decimal.NewFromInt32(count))
// 折扣价
discount := sku.Discount
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()
if user != nil && user.DiscountID != nil { // 用户特殊优惠
uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take()
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()
if uDiscountRate.Cmp(discountRate) > 0 {
if uDiscountRate.Cmp(discountRate) < 0 {
discountRate = uDiscountRate
discount = uDiscount
}
@@ -186,20 +192,20 @@ func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, c
var err error
coupon, err = Coupon.GetUserCoupon(user.ID, *cuid, discounted)
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)
}
// 约束到最低价格
if discounted.Cmp(sku.PriceMin) < 0 {
discounted = sku.PriceMin.Copy()
if discounted.Cmp(amountMin) < 0 {
discounted = amountMin.Copy()
}
if couponApplied.Cmp(sku.PriceMin) < 0 {
couponApplied = sku.PriceMin.Copy()
if couponApplied.Cmp(amountMin) < 0 {
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 {
@@ -260,7 +266,7 @@ func (data *CreateResourceData) Code() string {
}
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 {
return nil, err
}
@@ -277,7 +283,7 @@ func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error)
data,
m.TradeTypePurchase,
sku.Name,
amount, actual,
amount, discounted, actual,
discountId, couponUserId,
}, nil
}

View File

@@ -619,6 +619,7 @@ type TradeDetail struct {
Type m.TradeType `json:"type"`
Subject string `json:"subject"`
Amount decimal.Decimal `json:"amount"`
Discounted decimal.Decimal `json:"discounted"`
Actual decimal.Decimal `json:"actual"`
DiscountId *int32 `json:"discount_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) {
if data.Amount <= 0 {
return nil, core.NewBizErr("充值金额必须大于0")
}
amount := decimal.NewFromInt(int64(data.Amount)).Div(decimal.NewFromInt(100))
return &TradeDetail{
data,
m.TradeTypeRecharge,
fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)),
amount, amount,
amount, amount, amount,
nil, nil,
}, nil
}

View File

@@ -89,8 +89,8 @@ func (s *verifierService) SendSms(ctx context.Context, phone string, purpose Ver
return nil
}
func (s *verifierService) VerifySms(ctx context.Context, phone, code string) error {
key := smsKey(phone, VerifierSmsPurposeLogin)
func (s *verifierService) VerifySms(ctx context.Context, phone, code string, purpose VerifierSmsPurpose) error {
key := smsKey(phone, purpose)
keyLock := key + ":lock"
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
@@ -146,7 +146,8 @@ func smsKey(phone string, purpose VerifierSmsPurpose) string {
type VerifierSmsPurpose int
const (
VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录
VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录
VerifierSmsPurposePassword // 修改密码
)
// region 服务异常

View File

@@ -49,3 +49,11 @@ func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
}
return nil
}
func HandleClearExpiredChannels(_ context.Context, _ *asynq.Task) (err error) {
err = s.Channel.ClearExpiredChannels()
if err != nil {
return fmt.Errorf("清理过期通道失败: %w", err)
}
return nil
}

View File

@@ -42,6 +42,10 @@ func RunApp(pCtx context.Context) error {
return RunTask(ctx)
})
g.Go(func() error {
return RunCron(ctx)
})
return g.Wait()
}
@@ -90,6 +94,7 @@ func RunTask(ctx context.Context) error {
var mux = asynq.NewServeMux()
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
mux.HandleFunc(events.ClearExpiredChannels, tasks.HandleClearExpiredChannels)
// 停止服务
go func() {
@@ -105,3 +110,26 @@ func RunTask(ctx context.Context) error {
return nil
}
func RunCron(ctx context.Context) error {
server := asynq.NewSchedulerFromRedisClient(deps.Redis, &asynq.SchedulerOpts{
Location: time.Local,
})
// 每小时清理一次一小时之前的过期通道
server.Register("0 * * * *", asynq.NewTask(events.ClearExpiredChannels, nil))
// 停止服务
go func() {
<-ctx.Done()
server.Shutdown()
}()
// 启动服务
err := server.Run()
if err != nil {
return fmt.Errorf("定时任务服务运行失败: %w", err)
}
return nil
}