16 Commits

56 changed files with 2107 additions and 323 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,9 +15,93 @@ insert into admin (username, password, name, lock) values ('admin', '', '超级
-- region 产品 -- region 产品
-- ==================== -- ====================
insert into product (code, name, description) values ('short', '短效动态', '短效动态'); delete from product where true;
insert into product (code, name, description) values ('long', '长效动态', '长效动态');
insert into product (code, name, description) values ('static', '长效静', '长效静'); insert into product (code, name, description, sort) values ('short', '短效动', '短效动', 1);
insert into product (code, name, description, sort) values ('long', '长效动态', '长效动态', 2);
insert into product (code, name, description, sort) values ('static', '长效静态', '长效静态', 3);
-- ====================
-- region 套餐
-- ====================
delete from product_sku where true;
insert into product_sku (product_id, code, name, price, price_min, sort) values
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=3&expire=0', '短效动态包量 3 分钟', 10.00, 10.00, 1),
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=5&expire=0', '短效动态包量 5 分钟', 10.00, 10.00, 2),
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=10&expire=0', '短效动态包量 10 分钟', 10.00, 10.00, 3),
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=15&expire=0', '短效动态包量 15 分钟', 10.00, 10.00, 4),
((select id from product where code = 'short' and deleted_at is null), 'mode=quota&live=30&expire=0', '短效动态包量 30 分钟', 10.00, 10.00, 5),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=7', '短效动态包时 3 分钟 7 天', 10.00, 10.00, 6),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=7', '短效动态包时 5 分钟 7 天', 10.00, 10.00, 7),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=7', '短效动态包时 10 分钟 7 天', 10.00, 10.00, 8),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=7', '短效动态包时 15 分钟 7 天', 10.00, 10.00, 9),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=7', '短效动态包时 30 分钟 7 天', 10.00, 10.00, 10),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=15', '短效动态包时 3 分钟 15 天', 10.00, 10.00, 11),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=15', '短效动态包时 5 分钟 15 天', 10.00, 10.00, 12),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=15', '短效动态包时 10 分钟 15 天', 10.00, 10.00, 13),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=15', '短效动态包时 15 分钟 15 天', 10.00, 10.00, 14),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=15', '短效动态包时 30 分钟 15 天', 10.00, 10.00, 15),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=30', '短效动态包时 3 分钟 30 天', 10.00, 10.00, 16),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=30', '短效动态包时 5 分钟 30 天', 10.00, 10.00, 17),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=30', '短效动态包时 10 分钟 30 天', 10.00, 10.00, 18),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=30', '短效动态包时 15 分钟 30 天', 10.00, 10.00, 19),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=30', '短效动态包时 30 分钟 30 天', 10.00, 10.00, 20),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=90', '短效动态包时 3 分钟 90 天', 10.00, 10.00, 21),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=90', '短效动态包时 5 分钟 90 天', 10.00, 10.00, 22),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=90', '短效动态包时 10 分钟 90 天', 10.00, 10.00, 23),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=90', '短效动态包时 15 分钟 90 天', 10.00, 10.00, 24),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=90', '短效动态包时 30 分钟 90 天', 10.00, 10.00, 25),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=180', '短效动态包时 3 分钟 180 天', 10.00, 10.00, 26),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=180', '短效动态包时 5 分钟 180 天', 10.00, 10.00, 27),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=180', '短效动态包时 10 分钟 180 天', 10.00, 10.00, 28),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=180', '短效动态包时 15 分钟 180 天', 10.00, 10.00, 29),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=180', '短效动态包时 30 分钟 180 天', 10.00, 10.00, 30),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=3&expire=365', '短效动态包时 3 分钟 365 天', 10.00, 10.00, 31),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=5&expire=365', '短效动态包时 5 分钟 365 天', 10.00, 10.00, 32),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=10&expire=365', '短效动态包时 10 分钟 365 天', 10.00, 10.00, 33),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=15&expire=365', '短效动态包时 15 分钟 365 天', 10.00, 10.00, 34),
((select id from product where code = 'short' and deleted_at is null), 'mode=time&live=30&expire=365', '短效动态包时 30 分钟 365 天', 10.00, 10.00, 35)
;
insert into product_sku (product_id, code, name, price, price_min, sort) values
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=60&expire=0', '长效动态包量 1 小时', 10.00, 10.00, 1),
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=240&expire=0', '长效动态包量 4 小时', 10.00, 10.00, 2),
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=480&expire=0', '长效动态包量 8 小时', 10.00, 10.00, 3),
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=720&expire=0', '长效动态包量 12 小时', 10.00, 10.00, 4),
((select id from product where code = 'long' and deleted_at is null), 'mode=quota&live=1440&expire=0', '长效动态包量 24 小时', 10.00, 10.00, 5),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=7', '长效动态包时 1 小时 7 天', 10.00, 10.00, 6),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=7', '长效动态包时 4 小时 7 天', 10.00, 10.00, 7),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=7', '长效动态包时 8 小时 7 天', 10.00, 10.00, 8),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=7', '长效动态包时 12 小时 7 天', 10.00, 10.00, 9),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=7', '长效动态包时 24 小时 7 天', 10.00, 10.00, 10),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=15', '长效动态包时 1 小时 15 天', 10.00, 10.00, 11),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=15', '长效动态包时 4 小时 15 天', 10.00, 10.00, 12),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=15', '长效动态包时 8 小时 15 天', 10.00, 10.00, 13),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=15', '长效动态包时 12 小时 15 天', 10.00, 10.00, 14),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=15', '长效动态包时 24 小时 15 天', 10.00, 10.00, 15),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=30', '长效动态包时 1 小时 30 天', 10.00, 10.00, 16),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=30', '长效动态包时 4 小时 30 天', 10.00, 10.00, 17),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=30', '长效动态包时 8 小时 30 天', 10.00, 10.00, 18),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=30', '长效动态包时 12 小时 30 天', 10.00, 10.00, 19),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=30', '长效动态包时 24 小时 30 天', 10.00, 10.00, 20),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=90', '长效动态包时 1 小时 90 天', 10.00, 10.00, 21),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=90', '长效动态包时 4 小时 90 天', 10.00, 10.00, 22),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=90', '长效动态包时 8 小时 90 天', 10.00, 10.00, 23),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=90', '长效动态包时 12 小时 90 天', 10.00, 10.00, 24),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=90', '长效动态包时 24 小时 90 天', 10.00, 10.00, 25),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=180', '长效动态包时 1 小时 180 天', 10.00, 10.00, 26),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=180', '长效动态包时 4 小时 180 天', 10.00, 10.00, 27),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=180', '长效动态包时 8 小时 180 天', 10.00, 10.00, 28),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=180', '长效动态包时 12 小时 180 天', 10.00, 10.00, 29),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=180','长效动态包时 24 小时 180 天', 10.00, 10.00, 30),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=60&expire=365', '长效动态包时 1 小时 365 天', 10.00, 10.00, 31),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=240&expire=365', '长效动态包时 4 小时 365 天', 10.00, 10.00, 32),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=480&expire=365', '长效动态包时 8 小时 365 天', 10.00, 10.00, 33),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=720&expire=365', '长效动态包时 12 小时 365 天', 10.00, 10.00, 34),
((select id from product where code = 'long' and deleted_at is null), 'mode=time&live=1440&expire=365','长效动态包时 24 小时 365 天', 10.00, 10.00, 35)
;
-- ==================== -- ====================
-- region 权限 -- region 权限
@@ -42,7 +126,9 @@ insert into permission (name, description, sort) values
('channel', 'IP', 11), ('channel', 'IP', 11),
('trade', '交易', 12), ('trade', '交易', 12),
('bill', '账单', 13), ('bill', '账单', 13),
('balance_activity', '余额变动', 14); ('balance_activity', '余额变动', 14),
('proxy', '代理', 15),
('coupon_user', '已发放优惠券', 16);
-- -------------------------- -- --------------------------
-- level 2 -- level 2
@@ -51,74 +137,84 @@ insert into permission (name, description, sort) values
-- permission 子权限 -- permission 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- admin_role 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- admin 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- product 子权限
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' and deleted_at is null), 'product:read', '读取产品列表', 1), ((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 子权限 -- product_sku 子权限
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' and deleted_at is null), 'product_sku:read', '读取产品套餐列表', 1), ((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 子权限 -- discount 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- resource 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'resource' and deleted_at is null), 'resource:read', '读取用户套餐列表', 1), ((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:short', '短效动态套餐', 3),
((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4); ((select id from permission where name = 'resource' and deleted_at is null), 'resource:long', '长效动态套餐', 4);
-- user 子权限 -- user 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- coupon 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- batch 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- channel 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'channel' and deleted_at is null), 'channel:read', '读取 IP 列表', 1), ((select id from permission where name = 'channel' and deleted_at is null), 'channel:read', '读取 IP 列表', 1),
((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '写入 IP', 2); ((select id from permission where name = 'channel' and deleted_at is null), 'channel:write', '编辑 IP', 2);
-- proxy 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:read', '读取代理列表', 1),
((select id from permission where name = 'proxy' and deleted_at is null), 'proxy:write', '编辑代理', 2);
-- trade 子权限 -- trade 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'trade' and deleted_at is null), 'trade:read', '读取交易列表', 1), ((select id from permission where name = 'trade' and deleted_at is null), 'trade:read', '读取交易列表', 1),
((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 子权限 -- bill 子权限
insert into permission (parent_id, name, description, sort) values 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: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 子权限 -- balance_activity 子权限
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' and deleted_at is null), 'balance_activity:read', '读取余额变动列表', 1); ((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 -- level 3
-- -------------------------- -- --------------------------
@@ -127,6 +223,10 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'product_sku:write' and deleted_at is null), 'product_sku:write:status', '更改产品套餐状态', 1); ((select id from permission where name = 'product_sku:write' and deleted_at is null), 'product_sku:write:status', '更改产品套餐状态', 1);
-- proxy:write 子权限
insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'proxy:write' and deleted_at is null), 'proxy:write:status', '更改代理状态', 1);
-- resource:short 子权限 -- resource:short 子权限
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'resource:short' and deleted_at is null), 'resource:short:read', '读取用户短效动态套餐列表', 1); ((select id from permission where name = 'resource:short' and deleted_at is null), 'resource:short:read', '读取用户短效动态套餐列表', 1);
@@ -142,7 +242,7 @@ insert into permission (parent_id, name, description, sort) values
-- user:write 子权限 -- user:write 子权限
insert into permission (parent_id, name, description, sort) values 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); ((select id from permission where name = 'user:write' and deleted_at is null), 'user:write:bind', '用户认领', 2);
-- batch:read 子权限 -- batch:read 子权限
@@ -165,6 +265,14 @@ insert into permission (parent_id, name, description, sort) values
insert into permission (parent_id, name, description, sort) values insert into permission (parent_id, name, description, sort) values
((select id from permission where name = 'balance_activity:read' and deleted_at is null), 'balance_activity:read:of_user', '读取指定用户的余额变动列表', 1); ((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 -- level 4
-- -------------------------- -- --------------------------

View File

@@ -761,6 +761,8 @@ create table product_sku (
price decimal not null, price decimal not null,
price_min decimal not null, price_min decimal not null,
status int not null default 1, status int not null default 1,
sort int not null default 0,
count_min int not null default 1,
created_at timestamptz default current_timestamp, created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp, updated_at timestamptz default current_timestamp,
deleted_at timestamptz deleted_at timestamptz
@@ -778,6 +780,8 @@ comment on column product_sku.code is 'SKU 代码:格式为 key=value,key=valu
comment on column product_sku.name is 'SKU 可读名称'; comment on column product_sku.name is 'SKU 可读名称';
comment on column product_sku.price_min is '最低价格'; comment on column product_sku.price_min is '最低价格';
comment on column product_sku.status is 'SKU状态0-禁用1-正常'; comment on column product_sku.status is 'SKU状态0-禁用1-正常';
comment on column product_sku.sort is '排序';
comment on column product_sku.count_min is '最小购买数量';
comment on column product_sku.created_at is '创建时间'; comment on column product_sku.created_at is '创建时间';
comment on column product_sku.updated_at is '更新时间'; comment on column product_sku.updated_at is '更新时间';
comment on column product_sku.deleted_at is '删除时间'; comment on column product_sku.deleted_at is '删除时间';
@@ -814,6 +818,7 @@ create table resource (
code text, code text,
type int not null, type int not null,
active bool not null default true, active bool not null default true,
checkip bool not null default true,
created_at timestamptz default current_timestamp, created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp, updated_at timestamptz default current_timestamp,
deleted_at timestamptz deleted_at timestamptz
@@ -828,9 +833,10 @@ comment on table resource is '套餐表';
comment on column resource.id is '套餐ID'; comment on column resource.id is '套餐ID';
comment on column resource.user_id is '用户ID'; comment on column resource.user_id is '用户ID';
comment on column resource.resource_no is '套餐编号'; comment on column resource.resource_no is '套餐编号';
comment on column resource.active is '套餐状态';
comment on column resource.type is '套餐类型1-短效动态2-长效动态';
comment on column resource.code is '产品编码'; comment on column resource.code is '产品编码';
comment on column resource.type is '套餐类型1-短效动态2-长效动态';
comment on column resource.active is '套餐状态';
comment on column resource.checkip is '提取时是否检查 ip 地址';
comment on column resource.created_at is '创建时间'; comment on column resource.created_at is '创建时间';
comment on column resource.updated_at is '更新时间'; comment on column resource.updated_at is '更新时间';
comment on column resource.deleted_at is '删除时间'; comment on column resource.deleted_at is '删除时间';
@@ -1105,7 +1111,7 @@ comment on table coupon_user is '优惠券发放表';
comment on column coupon_user.id is '记录ID'; comment on column coupon_user.id is '记录ID';
comment on column coupon_user.coupon_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.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.expire_at is '过期时间';
comment on column coupon_user.used_at is '使用时间'; comment on column coupon_user.used_at is '使用时间';
comment on column coupon_user.created_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" "context"
"errors" "errors"
"log/slog" "log/slog"
"platform/pkg/u"
"platform/web/core" "platform/web/core"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
@@ -51,7 +50,6 @@ func authUser(loginType PwdLoginType, username, password string) (user *m.User,
if user == nil { if user == nil {
user = &m.User{ user = &m.User{
Phone: username, Phone: username,
Username: u.P(username),
Status: m.UserStatusEnabled, Status: m.UserStatusEnabled,
} }
} }
@@ -79,7 +77,7 @@ func authUser(loginType PwdLoginType, username, password string) (user *m.User,
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) { func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
// 验证验证码 // 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code) err := s.Verifier.VerifySms(context.Background(), username, code, s.VerifierSmsPurposeLogin)
if err != nil { if err != nil {
return nil, core.NewBizErr("短信认证失败", err) return nil, core.NewBizErr("短信认证失败", err)
} }

View File

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

View File

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

View File

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

View File

@@ -51,6 +51,12 @@ const (
ScopeCoupon = string("coupon") // 优惠券 ScopeCoupon = string("coupon") // 优惠券
ScopeCouponRead = string("coupon:read") // 读取优惠券列表 ScopeCouponRead = string("coupon:read") // 读取优惠券列表
ScopeCouponWrite = string("coupon:write") // 写入优惠券 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") // 批次 ScopeBatch = string("batch") // 批次
ScopeBatchRead = string("batch:read") // 读取批次列表 ScopeBatchRead = string("batch:read") // 读取批次列表
@@ -62,6 +68,11 @@ const (
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表 ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表
ScopeChannelWrite = string("channel:write") // 写入 IP ScopeChannelWrite = string("channel:write") // 写入 IP
ScopeProxy = string("proxy") // 代理
ScopeProxyRead = string("proxy:read") // 读取代理列表
ScopeProxyWrite = string("proxy:write") // 写入代理
ScopeProxyWriteStatus = string("proxy:write:status") // 更改代理状态
ScopeTrade = string("trade") // 交易 ScopeTrade = string("trade") // 交易
ScopeTradeRead = string("trade:read") // 读取交易列表 ScopeTradeRead = string("trade:read") // 读取交易列表
ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表 ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表

View File

@@ -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("未处理的异常", slog.String("type", t.String()), slog.String("error", err.Error()))
} }
slog.Warn(message)
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(code).SendString(message) return c.Status(code).SendString(message)
} }

View File

@@ -9,3 +9,9 @@ const RemoveChannel = "channel:remove"
func NewRemoveChannel(batch string) *asynq.Task { func NewRemoveChannel(batch string) *asynq.Task {
return asynq.NewTask(RemoveChannel, []byte(batch)) 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 return err
} }
var req PageAdminsReq var req core.PageReq
if err := g.Validator.ParseBody(c, &req); err != nil { if err := g.Validator.ParseBody(c, &req); err != nil {
return err return err
} }
list, total, err := s.Admin.Page(req.PageReq) list, total, err := s.Admin.Page(req)
if err != nil { if err != nil {
return err return err
} }
@@ -33,10 +33,6 @@ func PageAdminByAdmin(c *fiber.Ctx) error {
}) })
} }
type PageAdminsReq struct {
core.PageReq
}
func AllAdminByAdmin(c *fiber.Ctx) error { func AllAdminByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRead) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeAdminRead)
if err != nil { if err != nil {

View File

@@ -11,6 +11,65 @@ import (
"github.com/gofiber/fiber/v2" "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 分页查询所有余额变动记录 // PageBalanceActivityByAdmin 分页查询所有余额变动记录
func PageBalanceActivityByAdmin(c *fiber.Ctx) error { func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -30,6 +89,9 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
if req.UserPhone != nil { if req.UserPhone != nil {
do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone)) do = do.Where(q.User.As("User").Phone.Eq(*req.UserPhone))
} }
if req.BillNo != nil {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
}
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t)) do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
@@ -68,6 +130,7 @@ func PageBalanceActivityByAdmin(c *fiber.Ctx) error {
type PageBalanceActivityByAdminReq struct { type PageBalanceActivityByAdminReq struct {
core.PageReq core.PageReq
UserPhone *string `json:"user_phone,omitempty"` UserPhone *string `json:"user_phone,omitempty"`
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"` CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"` CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
} }
@@ -88,6 +151,9 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
// 构造查询条件 // 构造查询条件
do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(req.UserID)) do := q.BalanceActivity.Where(q.BalanceActivity.UserID.Eq(req.UserID))
if req.BillNo != nil {
do = do.Where(q.Bill.As("Bill").BillNo.Eq(*req.BillNo))
}
if req.CreatedAtStart != nil { if req.CreatedAtStart != nil {
t := u.DateHead(*req.CreatedAtStart) t := u.DateHead(*req.CreatedAtStart)
do = do.Where(q.BalanceActivity.CreatedAt.Gte(t)) do = do.Where(q.BalanceActivity.CreatedAt.Gte(t))
@@ -124,6 +190,7 @@ func PageBalanceActivityOfUserByAdmin(c *fiber.Ctx) error {
type PageBalanceActivityOfUserByAdminReq struct { type PageBalanceActivityOfUserByAdminReq struct {
core.PageReq core.PageReq
UserID int32 `json:"user_id" validate:"required"` UserID int32 `json:"user_id" validate:"required"`
BillNo *string `json:"bill_no,omitempty"`
CreatedAtStart *time.Time `json:"created_at_start,omitempty"` CreatedAtStart *time.Time `json:"created_at_start,omitempty"`
CreatedAtEnd *time.Time `json:"created_at_end,omitempty"` CreatedAtEnd *time.Time `json:"created_at_end,omitempty"`
} }

View File

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

View File

@@ -103,3 +103,27 @@ func DeleteCoupon(c *fiber.Ctx) error {
return nil 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 { type AllProductsByAdminReq struct {
} }
func AllProduct(c *fiber.Ctx) error {
infos, err := s.Product.AllProductSaleInfos()
if err != nil {
return err
}
return c.JSON(infos)
}
func CreateProduct(c *fiber.Ctx) error { func CreateProduct(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite) _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeProductWrite)
if err != nil { if err != nil {

View File

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

View File

@@ -84,6 +84,7 @@ func PageResourceShort(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset()) total = int64(len(resource) + req.GetOffset())
} else { } else {
total, err = q.Resource. total, err = q.Resource.
Joins(q.Resource.Short).
Where(do). Where(do).
Count() Count()
if err != nil { if err != nil {
@@ -180,6 +181,7 @@ func PageResourceLong(c *fiber.Ctx) error {
total = int64(len(resource) + req.GetOffset()) total = int64(len(resource) + req.GetOffset())
} else { } else {
total, err = q.Resource. total, err = q.Resource.
Joins(q.Resource.Long).
Where(do). Where(do).
Count() Count()
if err != nil { if err != nil {
@@ -609,6 +611,30 @@ func UpdateResourceByAdmin(c *fiber.Ctx) error {
return c.JSON(nil) 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 统计每日可用 // StatisticResourceFree 统计每日可用
func StatisticResourceFree(c *fiber.Ctx) error { func StatisticResourceFree(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -797,10 +823,7 @@ type CreateResourceReq struct {
// ResourcePrice 套餐价格 // ResourcePrice 套餐价格
func ResourcePrice(c *fiber.Ctx) error { func ResourcePrice(c *fiber.Ctx) error {
// 检查权限 // 检查权限
_, err := auth.GetAuthCtx(c).PermitSecretClient() ac := auth.GetAuthCtx(c)
if err != nil {
return err
}
// 解析请求参数 // 解析请求参数
var req = new(CreateResourceReq) var req = new(CreateResourceReq)
@@ -809,7 +832,7 @@ func ResourcePrice(c *fiber.Ctx) error {
} }
// 获取套餐价格 // 获取套餐价格
detail, err := req.TradeDetail(nil) detail, err := req.TradeDetail(ac.User)
if err != nil { if err != nil {
return err return err
} }
@@ -817,11 +840,13 @@ func ResourcePrice(c *fiber.Ctx) error {
// 计算折扣 // 计算折扣
return c.JSON(ResourcePriceResp{ return c.JSON(ResourcePriceResp{
Price: detail.Amount.StringFixed(2), Price: detail.Amount.StringFixed(2),
Discounted: detail.Actual.StringFixed(2), Discounted: detail.Discounted.StringFixed(2),
Actual: detail.Actual.StringFixed(2),
}) })
} }
type ResourcePriceResp struct { type ResourcePriceResp struct {
Price string `json:"price"` Price string `json:"price"`
Discounted string `json:"discounted_price"` Discounted string `json:"discounted"`
Actual string `json:"actual"`
} }

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"errors" "errors"
"fmt"
"platform/pkg/env" "platform/pkg/env"
"platform/pkg/u" "platform/pkg/u"
"platform/web/auth" "platform/web/auth"
@@ -97,6 +98,18 @@ func CreateWhitelist(c *fiber.Ctx) error {
} }
// 创建白名单 // 创建白名单
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{ err = q.Whitelist.Create(&m.Whitelist{
UserID: authCtx.User.ID, UserID: authCtx.User.ID,
IP: u.Z(ip), IP: u.Z(ip),
@@ -106,6 +119,12 @@ func CreateWhitelist(c *fiber.Ctx) error {
return core.NewServErr("添加白名单失败", err) return core.NewServErr("添加白名单失败", err)
} }
return nil
})
if err != nil {
return err
}
return nil return nil
} }
@@ -206,3 +225,7 @@ func secureAddr(str string) (*orm.Inet, error) {
} }
return ip, nil return ip, nil
} }
func whitelistKey(userID int32) string {
return fmt.Sprintf("platform:whitelist:add:%d", userID)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import (
"platform/pkg/env" "platform/pkg/env"
auth2 "platform/web/auth" auth2 "platform/web/auth"
"platform/web/handlers" "platform/web/handlers"
s "platform/web/services"
"time" "time"
q "platform/web/queries" q "platform/web/queries"
@@ -25,7 +26,6 @@ func ApplyRouters(app *fiber.App) {
if env.RunMode == env.RunModeDev { if env.RunMode == env.RunModeDev {
debug := app.Group("/debug") debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode) debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear) debug.Get("/iden/clear/:phone", handlers.DebugIdentifyClear)
debug.Get("/session/now", func(ctx *fiber.Ctx) error { debug.Get("/session/now", func(ctx *fiber.Ctx) error {
rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now())).Find() rs, err := q.Session.Where(q.Session.AccessTokenExpires.Gt(time.Now())).Find()
@@ -34,6 +34,12 @@ func ApplyRouters(app *fiber.App) {
} }
return ctx.JSON(rs) 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/short", handlers.PageResourceShort)
resource.Post("/list/long", handlers.PageResourceLong) resource.Post("/list/long", handlers.PageResourceLong)
resource.Post("/create", handlers.CreateResource) resource.Post("/create", handlers.CreateResource)
resource.Post("/update/checkip", handlers.UpdateResourceCheckIP)
resource.Post("/statistics/free", handlers.StatisticResourceFree) resource.Post("/statistics/free", handlers.StatisticResourceFree)
resource.Post("/statistics/usage", handlers.StatisticResourceUsage) resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
@@ -91,6 +98,15 @@ func userRouter(api fiber.Router) {
bill := api.Group("/bill") bill := api.Group("/bill")
bill.Post("/list", handlers.ListBill) 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 := api.Group("/announcement")
announcement.Post("/list", handlers.ListAnnouncements) announcement.Post("/list", handlers.ListAnnouncements)
@@ -109,6 +125,14 @@ func userRouter(api fiber.Router) {
// 前台 // 前台
inquiry := api.Group("/inquiry") inquiry := api.Group("/inquiry")
inquiry.Post("/create", handlers.CreateInquiry) inquiry.Post("/create", handlers.CreateInquiry)
// 产品
product := api.Group("/product")
product.Post("/list", handlers.AllProduct)
// 认证
verify := api.Group("/verify")
verify.Post("/sms/password", handlers.SendSmsCodeForPassword)
} }
// 客户端接口路由 // 客户端接口路由
@@ -116,7 +140,7 @@ func clientRouter(api fiber.Router) {
client := api client := api
// 验证短信令牌 // 验证短信令牌
client.Post("/verify/sms", handlers.SmsCode) client.Post("/verify/sms", handlers.SendSmsCode)
// 套餐定价查询 // 套餐定价查询
resource := client.Group("/resource") resource := client.Group("/resource")
@@ -126,9 +150,6 @@ func clientRouter(api fiber.Router) {
channel := client.Group("/channel") channel := client.Group("/channel")
channel.Post("/remove", handlers.RemoveChannels) channel.Post("/remove", handlers.RemoveChannels)
// 代理网关注册
proxy := client.Group("/proxy")
proxy.Post("/register/baidyin", handlers.ProxyRegisterBaiYin)
} }
// 管理员接口路由 // 管理员接口路由
@@ -188,6 +209,15 @@ func adminRouter(api fiber.Router) {
channel.Post("/page", handlers.PageChannelByAdmin) channel.Post("/page", handlers.PageChannelByAdmin)
channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin) channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin)
// proxy 代理
var proxy = api.Group("/proxy")
proxy.Post("/all", handlers.AllProxyByAdmin)
proxy.Post("/page", handlers.PageProxyByAdmin)
proxy.Post("/create", handlers.CreateProxy)
proxy.Post("/update", handlers.UpdateProxy)
proxy.Post("/update/status", handlers.UpdateProxyStatus)
proxy.Post("/remove", handlers.RemoveProxy)
// trade 交易 // trade 交易
var trade = api.Group("/trade") var trade = api.Group("/trade")
trade.Post("/page", handlers.PageTradeByAdmin) trade.Post("/page", handlers.PageTradeByAdmin)
@@ -235,4 +265,14 @@ func adminRouter(api fiber.Router) {
coupon.Post("/create", handlers.CreateCoupon) coupon.Post("/create", handlers.CreateCoupon)
coupon.Post("/update", handlers.UpdateCoupon) coupon.Post("/update", handlers.UpdateCoupon)
coupon.Post("/remove", handlers.DeleteCoupon) 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, TradeID: &tradeId,
Type: m.BillTypeRecharge, Type: m.BillTypeRecharge,
Info: &detail.Subject, Info: &detail.Subject,
Amount: detail.Amount, Amount: detail.Discounted,
Actual: detail.Actual, Actual: detail.Actual,
} }
@@ -37,7 +37,7 @@ func (s *billService) CreateForResource(q *q.Query, uid, resourceId int32, trade
CouponUserID: detail.CouponUserId, CouponUserID: detail.CouponUserId,
Type: m.BillTypeConsume, Type: m.BillTypeConsume,
Info: &detail.Subject, Info: &detail.Subject,
Amount: detail.Amount, Amount: detail.Discounted,
Actual: detail.Actual, Actual: detail.Actual,
} }

View File

@@ -24,22 +24,27 @@ var Channel = &channelServer{
} }
type ChannelServiceProvider interface { type ChannelServiceProvider interface {
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string) error RemoveChannels(batch string) error
ClearExpiredChannels() error
} }
type channelServer struct { type channelServer struct {
provider ChannelServiceProvider provider ChannelServiceProvider
} }
func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) { func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter...) return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter)
} }
func (s *channelServer) RemoveChannels(batch string) error { func (s *channelServer) RemoveChannels(batch string) error {
return s.provider.RemoveChannels(batch) return s.provider.RemoveChannels(batch)
} }
func (s *channelServer) ClearExpiredChannels() error {
return s.provider.ClearExpiredChannels()
}
// 授权方式 // 授权方式
type ChannelAuthType int type ChannelAuthType int
@@ -87,6 +92,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
User: *resource.User, User: *resource.User,
Active: resource.Active, Active: resource.Active,
Type: resource.Type, Type: resource.Type,
CheckIP: resource.CheckIP,
} }
switch resource.Type { switch resource.Type {
@@ -94,7 +100,7 @@ func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
var sub = resource.Short var sub = resource.Short
info.ShortId = &sub.ID info.ShortId = &sub.ID
info.ExpireAt = sub.ExpireAt info.ExpireAt = sub.ExpireAt
info.Live = time.Duration(sub.Live) * time.Second info.Live = time.Duration(sub.Live) * time.Minute
info.Mode = sub.Type info.Mode = sub.Type
info.Quota = sub.Quota info.Quota = sub.Quota
info.Used = sub.Used info.Used = sub.Used
@@ -142,10 +148,11 @@ type ResourceView struct {
Daily int32 Daily int32
LastAt *time.Time LastAt *time.Time
Today int // 今日用量 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 { if count > 400 {
return nil, nil, core.NewBizErr("单次最多提取 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 return nil, nil, err
} }
if authWhitelist && len(whitelists) == 0 {
return nil, nil, core.NewBizErr("当前白名单为空,请先添加白名单")
}
ips := make([]string, len(whitelists)) ips := make([]string, len(whitelists))
pass := false pass := false
for i, item := range whitelists { for i, item := range whitelists {
@@ -178,7 +189,7 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
pass = true pass = true
} }
} }
if !pass { if resource.CheckIP && !pass {
return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String())) return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
} }
@@ -232,7 +243,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
// 缩容通道 // 缩容通道
func remChans(proxy int32) error { func remChans(proxy int32) error {
key := freeChansKey + ":" + strconv.Itoa(int(proxy)) key := freeChansKey + ":" + strconv.Itoa(int(proxy))
err := g.Redis.SRem(context.Background(), key).Err() err := g.Redis.Del(context.Background(), key).Err()
if err != nil { if err != nil {
return fmt.Errorf("缩容通道失败: %w", err) return fmt.Errorf("缩容通道失败: %w", err)
} }
@@ -272,7 +283,8 @@ local free_key = KEYS[1]
local batch_key = KEYS[2] local batch_key = KEYS[2]
local count = tonumber(ARGV[1]) local count = tonumber(ARGV[1])
if redis.call("SCARD", free_key) < count then local free_count = redis.call("SCARD", free_key)
if count <= 0 or free_count < count then
return nil return nil
end end
@@ -305,12 +317,13 @@ local free_key = KEYS[1]
local batch_key = KEYS[2] local batch_key = KEYS[2]
local chans = redis.call("LRANGE", batch_key, 0, -1) local chans = redis.call("LRANGE", batch_key, 0, -1)
redis.call("DEL", batch_key) if #chans == 0 then
return 1
if redis.call("EXISTS", free_key) == 1 then
redis.call("SADD", free_key, unpack(chans))
end end
redis.call("SADD", free_key, unpack(chans))
redis.call("DEL", batch_key)
return 1 return 1
`) `)

View File

@@ -17,23 +17,21 @@ import (
"time" "time"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"gorm.io/gen"
"gorm.io/gen/field" "gorm.io/gen/field"
) )
type channelBaiyinProvider struct{} type channelBaiyinProvider struct{}
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) { func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, filter *EdgeFilter) ([]*m.Channel, error) {
var filter *EdgeFilter = nil if filter == nil {
if len(edgeFilter) > 0 { return nil, core.NewBizErr("缺少节点过滤条件")
filter = &edgeFilter[0]
} }
now := time.Now() now := time.Now()
batch := ID.GenReadable("bat") batch := ID.GenReadable("bat")
// 检查并获取套餐与白名单 // 检查并获取套餐与白名单
resource, whitelists, err := ensure(now, source, resourceId, count) resource, whitelists, err := ensure(now, source, resourceId, authWhitelist, count)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,44 +62,57 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
} }
proxy := proxyResult.Proxy proxy := proxyResult.Proxy
// 获取可用通道 // 取用端口
chans, err := lockChans(proxy.ID, batch, count) var chans []netip.AddrPort
err = g.Redsync.WithLock(proxyStatusLockKey(proxy.ID), func() error {
lockedProxy, err := q.Proxy.Where(q.Proxy.ID.Eq(proxy.ID)).Take()
if err != nil { if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err) return err
}
if lockedProxy.Status != m.ProxyStatusOnline {
return core.NewBizErr("无可用主机,请稍后再试")
} }
// 获取可用节点 chans, err = lockChans(proxy.ID, batch, count)
edgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{ if err != nil {
Province: filter.Prov, return core.NewBizErr("无可用通道,请稍后再试", err)
City: filter.City, }
Isp: u.X(filter.Isp.String()),
Limit: &count, proxy = *lockedProxy
NoRepeat: u.P(true), return nil
NoDayRepeat: u.P(true),
ActiveTime: u.P(3600),
IpUnchangedTime: u.P(3600),
Sort: u.P("ip_unchanged_time_asc"),
}) })
if err != 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) channels := make([]*m.Channel, count)
chanConfigs := make([]*g.PortConfigsReq, count) chanConfigs := make([]*g.PortConfigsReq, count)
edgeConfigs := make([]string, count) edgeConfigs := make([]string, 0, count)
for i := range count { for i := range count {
ch := chans[i] ch := chans[i]
edge := edges[i] edge := edges[i]
if err != nil {
return nil, core.NewBizErr("解析通道地址失败", err)
}
// 通道数据 // 通道数据
channels[i] = &m.Channel{ channels[i] = &m.Channel{
UserID: user.ID, UserID: user.ID,
@@ -115,6 +126,7 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
FilterProv: filter.Prov, FilterProv: filter.Prov,
FilterCity: filter.City, FilterCity: filter.City,
ExpiredAt: expire, ExpiredAt: expire,
Proxy: &proxy,
} }
// 通道配置数据 // 通道配置数据
@@ -139,29 +151,18 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
} }
// 连接配置数据 // 连接配置数据
edgeConfigs[i] = edge.EdgeID if edge.Type == EdgeInfoCloud {
edgeConfigs = append(edgeConfigs, edge.EdgeID)
} }
// 提交异步任务关闭通道
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batch),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("提交关闭通道任务失败", err)
} }
// 保存数据 // 保存数据
err = q.Q.Transaction(func(q *q.Query) error { err = q.Q.Transaction(func(q *q.Query) error {
var rs gen.ResultInfo // 更新使用记录
var err error
// 根据套餐类型和模式更新使用记录 switch resource.Type {
isShortType := resource.Type == m.ResourceTypeShort case m.ResourceTypeShort:
isLongType := resource.Type == m.ResourceTypeLong _, err = q.ResourceShort.
switch {
case isShortType:
rs, err = q.ResourceShort.
Where( Where(
q.ResourceShort.ID.Eq(*resource.ShortId), q.ResourceShort.ID.Eq(*resource.ShortId),
q.ResourceShort.Used.Eq(resource.Used), q.ResourceShort.Used.Eq(resource.Used),
@@ -173,8 +174,8 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
q.ResourceShort.LastAt.Value(now), q.ResourceShort.LastAt.Value(now),
) )
case isLongType: case m.ResourceTypeLong:
rs, err = q.ResourceLong. _, err = q.ResourceLong.
Where( Where(
q.ResourceLong.ID.Eq(*resource.LongId), q.ResourceLong.ID.Eq(*resource.LongId),
q.ResourceLong.Used.Eq(resource.Used), q.ResourceLong.Used.Eq(resource.Used),
@@ -192,9 +193,6 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
if err != nil { if err != nil {
return core.NewServErr("更新套餐使用记录失败", err) return core.NewServErr("更新套餐使用记录失败", err)
} }
if rs.RowsAffected == 0 {
return core.NewServErr("套餐使用记录不存在")
}
// 保存通道 // 保存通道
err = q.Channel. 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 { if env.RunMode == env.RunModeProd {
// 连接节点到网关 // 连接节点到网关
@@ -243,10 +239,11 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
// 启用网关代理通道 // 启用网关代理通道
err = gateway.GatewayPortConfigs(chanConfigs) err = gateway.GatewayPortConfigs(chanConfigs)
if err != nil { if err != nil {
slog.Warn("提交代理端口配置失败", "error", err.Error())
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err) return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
} }
} else { } else {
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String()) slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "count", len(chanConfigs), "remote", len(edgeConfigs))
for _, item := range chanConfigs { for _, item := range chanConfigs {
str, _ := json.Marshal(item) str, _ := json.Marshal(item)
fmt.Println(string(str)) fmt.Println(string(str))
@@ -294,21 +291,25 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
// 提交配置 // 提交配置
if env.RunMode == env.RunModeProd { 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]) gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
err := gateway.GatewayPortConfigs(configs) err := gateway.GatewayPortConfigs(configs)
if err != nil { if err != nil {
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err) return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
} }
// 断开节点连接
_, err = g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
if err != nil {
slog.Warn("断开云平台连接失败", "error", err.Error())
return core.NewServErr("断开云平台连接失败", err)
}
} else { } else {
slog.Debug("清除代理端口配置", "proxy", proxy.IP)
for _, item := range configs { for _, item := range configs {
str, _ := json.Marshal(item) str, _ := json.Marshal(item)
fmt.Println(string(str)) fmt.Println(string(str))
@@ -321,6 +322,91 @@ func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
return err return err
} }
slog.Debug("清除代理端口配置", "time", time.Since(start).String()) slog.Debug("清除代理端口配置", "proxy", proxy.IP.String(), "batch", batch, "duration", time.Since(start).String())
return nil 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))) do = append(do, q.Coupon.Status.Value(int(*data.Status)))
} }
if data.ExpireType != nil { 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))) 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...) _, err := q.Coupon.Where(q.Coupon.ID.Eq(data.ID)).UpdateSimple(do...)
return err return err
@@ -98,6 +108,13 @@ func (s *couponService) Delete(id int32) error {
return err return err
} }
func (s *couponService) Assign(couponID int32, userID int32) error {
return CouponUser.Create(CreateCouponUserData{
CouponID: couponID,
UserID: userID,
})
}
// GetUserCoupon 获取用户的指定优惠券 // GetUserCoupon 获取用户的指定优惠券
func (s *couponService) GetUserCoupon(uid int32, cuid int32, amount decimal.Decimal) (*m.CouponUser, error) { 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.ID.Eq(cuid),
q.CouponUser.UserID.Eq(uid), q.CouponUser.UserID.Eq(uid),
q.CouponUser.Status.Eq(int(m.CouponUserStatusUnused)), 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() ).Take()
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("优惠券不存在或已失效") 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() Find()
} }
func (s *productService) AllProductSaleInfos() ([]*m.Product, error) {
products, err := q.Product.
Select(
q.Product.ID,
q.Product.Code,
q.Product.Name,
q.Product.Description,
q.Product.Sort,
).
Where(
q.Product.Status.Eq(int(m.ProductStatusEnabled)),
).
Order(q.Product.Sort).
Find()
if err != nil {
return nil, err
}
pids := make([]int32, len(products))
for i, p := range products {
pids[i] = p.ID
}
skus, err := q.ProductSku.
Select(
q.ProductSku.ID,
q.ProductSku.ProductID,
q.ProductSku.Name,
q.ProductSku.Code,
q.ProductSku.Price,
q.ProductSku.CountMin,
).
Where(
q.ProductSku.ProductID.In(pids...),
q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled)),
).
Order(q.ProductSku.Sort).
Find()
if err != nil {
return nil, err
}
pmap := make(map[int32]*m.Product, len(products))
for _, p := range products {
pmap[p.ID] = p
p.Skus = make([]*m.ProductSku, 0)
}
for _, s := range skus {
if p, ok := pmap[s.ProductID]; ok {
p.Skus = append(p.Skus, s)
}
}
return products, nil
}
// 新增产品 // 新增产品
func (s *productService) CreateProduct(create *CreateProductData) error { func (s *productService) CreateProduct(create *CreateProductData) error {
return q.Product.Create(&m.Product{ return q.Product.Create(&m.Product{

View File

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

View File

@@ -1,65 +1,216 @@
package services package services
import ( import (
"context"
"fmt" "fmt"
"net/netip" "net/netip"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
g "platform/web/globals"
"platform/web/globals/orm" "platform/web/globals/orm"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strconv"
"time" "time"
"gorm.io/gen/field"
) )
var Proxy = &proxyService{} var Proxy = &proxyService{}
type proxyService struct{} type proxyService struct{}
// AllProxies 获取所有代理 func proxyStatusLockKey(id int32) string {
func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Proxy, error) { return fmt.Sprintf("platform:proxy:status:%d", id)
proxies, err := q.Proxy.Where( }
q.Proxy.Type.Eq(int(proxyType)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)), func hasUsedChans(proxyID int32) (bool, error) {
).Preload( ctx := context.Background()
q.Proxy.Channels.On(q.Channel.ExpiredAt.Gte(time.Now())), pattern := usedChansKey + ":" + strconv.Itoa(int(proxyID)) + ":*"
).Find() keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result()
if err != nil { if err != nil {
return nil, err return false, err
}
return len(keys) > 0, nil
} }
return proxies, nil func rebuildFreeChans(proxyID int32, addr netip.Addr) error {
if err := remChans(proxyID); err != nil {
return err
} }
// 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,
}
if err := q.Proxy.Create(proxy); err != nil {
return core.NewServErr("保存通道数据失败")
}
// 添加可用通道到 redis
chans := make([]netip.AddrPort, 10000) chans := make([]netip.AddrPort, 10000)
for i := range 10000 { for i := range 10000 {
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000)) chans[i] = netip.AddrPortFrom(addr, uint16(i+10000))
} }
err := regChans(proxy.ID, chans)
if err := regChans(proxyID, chans); err != nil {
return err
}
return nil
}
func (s *proxyService) Page(req core.PageReq) (result []*m.Proxy, count int64, err error) {
return q.Proxy.
Omit(q.Proxy.Version, q.Proxy.Meta).
Order(q.Proxy.CreatedAt.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
}
func (s *proxyService) All() (result []*m.Proxy, err error) {
return q.Proxy.
Omit(q.Proxy.Version, q.Proxy.Meta).
Order(q.Proxy.CreatedAt.Desc()).
Find()
}
type CreateProxy struct {
Mac string `json:"mac" validate:"required"`
IP string `json:"ip" validate:"required"`
Host *string `json:"host"`
Secret *string `json:"secret"`
Type *m.ProxyType `json:"type"`
Status *m.ProxyStatus `json:"status"`
}
func (s *proxyService) Create(create *CreateProxy) error {
addr, err := netip.ParseAddr(create.IP)
if err != nil { if err != nil {
return core.NewServErr("添加通道失败", err) return core.NewServErr("IP地址格式错误", err)
} }
return q.Q.Transaction(func(tx *q.Query) error {
proxy := &m.Proxy{
Mac: create.Mac,
IP: orm.Inet{Addr: addr},
Host: create.Host,
Secret: create.Secret,
Type: u.Else(create.Type, m.ProxyTypeSelfHosted),
Status: u.Else(create.Status, m.ProxyStatusOffline),
}
if err := tx.Proxy.Create(proxy); err != nil {
return core.NewServErr("保存代理数据失败", err)
}
if err := rebuildFreeChans(proxy.ID, addr); err != nil {
return core.NewServErr("初始化代理通道失败", err)
}
return nil
})
}
type UpdateProxy struct {
ID int32 `json:"id" validate:"required"`
Mac *string `json:"mac"`
IP *string `json:"ip"`
Host *string `json:"host"`
Secret *string `json:"secret"`
}
func (s *proxyService) Update(update *UpdateProxy) error {
simples := make([]field.AssignExpr, 0)
hasSideEffect := false
if update.Mac != nil {
hasSideEffect = true
simples = append(simples, q.Proxy.Mac.Value(*update.Mac))
}
if update.IP != nil {
addr, err := netip.ParseAddr(*update.IP)
if err != nil {
return core.NewServErr("IP地址格式错误", err)
}
hasSideEffect = true
simples = append(simples, q.Proxy.IP.Value(orm.Inet{Addr: addr}))
}
if update.Host != nil {
simples = append(simples, q.Proxy.Host.Value(*update.Host))
}
if update.Secret != nil {
hasSideEffect = true
simples = append(simples, q.Proxy.Secret.Value(*update.Secret))
}
if len(simples) == 0 {
return nil return nil
} }
// UnregisterBaiyin 注销代理服务 if hasSideEffect {
func (s *proxyService) UnregisterBaiyin(id int) error { used, err := hasUsedChans(update.ID)
if err != nil {
return core.NewServErr("检查代理通道状态失败", err)
}
if used {
return core.NewBizErr("代理存在未关闭通道,禁止修改")
}
}
rs, err := q.Proxy.
Where(
q.Proxy.ID.Eq(update.ID),
q.Proxy.Status.Eq(int(m.ProxyStatusOffline)),
).
UpdateSimple(simples...)
if err != nil {
return err
}
if rs.RowsAffected == 0 {
return core.NewBizErr("代理未下线,禁止修改")
}
return nil return nil
} }
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 ( import (
"fmt" "fmt"
"log/slog"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
m "platform/web/models" m "platform/web/models"
@@ -62,9 +63,10 @@ func (s *resourceService) Create(q *q.Query, uid int32, now time.Time, data *Cre
var resource = m.Resource{ var resource = m.Resource{
UserID: uid, UserID: uid,
ResourceNo: u.P(ID.GenReadable("res")), ResourceNo: u.P(ID.GenReadable("res")),
Active: true,
Type: data.Type, Type: data.Type,
Code: data.Type.Code(), Code: data.Type.Code(),
Active: true,
CheckIP: true,
} }
switch data.Type { 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 { func (s *resourceService) Update(data *UpdateResourceData) error {
if data.Active == nil {
return core.NewBizErr("更新套餐失败active 不能为空")
}
do := make([]field.AssignExpr, 0) do := make([]field.AssignExpr, 0)
if data.Active != nil { if data.Active != nil {
do = append(do, q.Resource.Active.Value(*data.Active)) do = append(do, q.Resource.Active.Value(*data.Active))
} }
if data.CheckIP != nil {
do = append(do, q.Resource.CheckIP.Value(*data.CheckIP))
}
_, err := q.Resource. _, err := q.Resource.
Where(q.Resource.ID.Eq(data.Id)). Where(q.Resource.ID.Eq(data.Id)).
@@ -142,37 +143,42 @@ func (s *resourceService) Update(data *UpdateResourceData) error {
type UpdateResourceData struct { type UpdateResourceData struct {
core.IdReq core.IdReq
Active *bool `json:"active"` Active *bool `json:"active"`
CheckIP *bool `json:"checkip"`
} }
func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, cuid *int32) (*m.ProductSku, *m.ProductDiscount, *m.CouponUser, decimal.Decimal, decimal.Decimal, error) { func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, cuid *int32) (*m.ProductSku, *m.ProductDiscount, *m.CouponUser, decimal.Decimal, decimal.Decimal, decimal.Decimal, error) {
if count <= 0 {
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr("购买数量必须大于 0")
}
sku, err := q.ProductSku. sku, err := q.ProductSku.
Joins(q.ProductSku.Discount). Joins(q.ProductSku.Discount).
Where(q.ProductSku.Code.Eq(skuCode), q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled))). Where(q.ProductSku.Code.Eq(skuCode), q.ProductSku.Status.Eq(int32(m.SkuStatusEnabled))).
Take() Take()
if err != nil { if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("产品不可用", err) slog.Debug("查询产品失败", "skuCode", skuCode)
return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewBizErr("产品不可用", err)
} }
// 原价 // 原价
price := sku.Price amountMin := sku.PriceMin.Mul(decimal.NewFromInt32(count))
amount := price.Mul(decimal.NewFromInt32(count)) amount := sku.Price.Mul(decimal.NewFromInt32(count))
// 折扣价 // 折扣价
discount := sku.Discount discount := sku.Discount
if discount == nil { if discount == nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("价格查询失败", err) return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("产品未配置折扣", err)
} }
discountRate := discount.Rate() discountRate := discount.Rate()
if user != nil && user.DiscountID != nil { // 用户特殊优惠 if user != nil && user.DiscountID != nil { // 用户特殊优惠
uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take() uDiscount, err := q.ProductDiscount.Where(q.ProductDiscount.ID.Eq(*user.DiscountID)).Take()
if err != nil { if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err) return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, core.NewServErr("客户特殊价查询失败", err)
} }
uDiscountRate := uDiscount.Rate() uDiscountRate := uDiscount.Rate()
if uDiscountRate.Cmp(discountRate) > 0 { if uDiscountRate.Cmp(discountRate) < 0 {
discountRate = uDiscountRate discountRate = uDiscountRate
discount = uDiscount discount = uDiscount
} }
@@ -186,20 +192,20 @@ func (s *resourceService) CalcPrice(skuCode string, count int32, user *m.User, c
var err error var err error
coupon, err = Coupon.GetUserCoupon(user.ID, *cuid, discounted) coupon, err = Coupon.GetUserCoupon(user.ID, *cuid, discounted)
if err != nil { if err != nil {
return nil, nil, nil, decimal.Zero, decimal.Zero, err return nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, err
} }
couponApplied = discounted.Sub(coupon.Coupon.Amount) couponApplied = discounted.Sub(coupon.Coupon.Amount)
} }
// 约束到最低价格 // 约束到最低价格
if discounted.Cmp(sku.PriceMin) < 0 { if discounted.Cmp(amountMin) < 0 {
discounted = sku.PriceMin.Copy() discounted = amountMin.Copy()
} }
if couponApplied.Cmp(sku.PriceMin) < 0 { if couponApplied.Cmp(amountMin) < 0 {
couponApplied = sku.PriceMin.Copy() couponApplied = amountMin.Copy()
} }
return sku, discount, coupon, discounted, couponApplied, nil return sku, discount, coupon, amount.RoundCeil(2), discounted.RoundCeil(2), couponApplied.RoundCeil(2), nil
} }
type CreateResourceData struct { type CreateResourceData struct {
@@ -260,7 +266,7 @@ func (data *CreateResourceData) Code() string {
} }
func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) { func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error) {
sku, discount, coupon, amount, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponId) sku, discount, coupon, amount, discounted, actual, err := Resource.CalcPrice(data.Code(), data.Count(), user, data.CouponId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -277,7 +283,7 @@ func (data *CreateResourceData) TradeDetail(user *m.User) (*TradeDetail, error)
data, data,
m.TradeTypePurchase, m.TradeTypePurchase,
sku.Name, sku.Name,
amount, actual, amount, discounted, actual,
discountId, couponUserId, discountId, couponUserId,
}, nil }, nil
} }

View File

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

View File

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

View File

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

View File

@@ -49,3 +49,11 @@ func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
} }
return nil 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) return RunTask(ctx)
}) })
g.Go(func() error {
return RunCron(ctx)
})
return g.Wait() return g.Wait()
} }
@@ -90,6 +94,7 @@ func RunTask(ctx context.Context) error {
var mux = asynq.NewServeMux() var mux = asynq.NewServeMux()
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel) mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade) mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
mux.HandleFunc(events.ClearExpiredChannels, tasks.HandleClearExpiredChannels)
// 停止服务 // 停止服务
go func() { go func() {
@@ -105,3 +110,26 @@ func RunTask(ctx context.Context) error {
return nil 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
}