Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ae9ad40de |
@@ -10,6 +10,12 @@ GRAFANA_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
|
||||
# PPanel Server 镜像标签(由 CI/CD 传入不可变 tag,如 git SHA)
|
||||
PPANEL_SERVER_TAG=CHANGE_ME_TO_GIT_SHA
|
||||
|
||||
# 测试环境 MinIO / S3 兼容存储
|
||||
# ppanel.yaml 中 S3.Endpoint 使用 http://127.0.0.1:9000,Bucket 与此处保持一致。
|
||||
MINIO_ROOT_USER=CHANGE_ME_TO_S3_ACCESS_KEY
|
||||
MINIO_ROOT_PASSWORD=CHANGE_ME_TO_S3_SECRET_KEY
|
||||
MINIO_BUCKET=hifastvpn
|
||||
|
||||
# AWS 区域(香港)
|
||||
AWS_REGION=ap-east-1
|
||||
|
||||
|
||||
+33
-14
@@ -76,6 +76,29 @@ type (
|
||||
GiftAmount int64 `json:"gift_amount"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
UserSubscribeDetail {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User User `json:"user"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
NodeGroupId int64 `json:"node_group_id"`
|
||||
GroupLocked bool `json:"group_locked"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
EffectiveSpeed int64 `json:"effective_speed"`
|
||||
IsThrottled bool `json:"is_throttled"`
|
||||
ThrottleRule string `json:"throttle_rule,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
BatchDeleteUserRequest {
|
||||
Ids []int64 `json:"ids" validate:"required"`
|
||||
}
|
||||
@@ -135,22 +158,18 @@ type (
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
CreateUserSubscribeRequest {
|
||||
UserId int64 `json:"user_id"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
SpeedLimit int64 `json:"speed_limit,optional"`
|
||||
TrafficLimit string `json:"traffic_limit,optional"`
|
||||
UserId int64 `json:"user_id"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
}
|
||||
UpdateUserSubscribeRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Upload int64 `json:"upload"`
|
||||
Download int64 `json:"download"`
|
||||
SpeedLimit *int64 `json:"speed_limit,omitempty"`
|
||||
TrafficLimit *string `json:"traffic_limit,omitempty"`
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Upload int64 `json:"upload"`
|
||||
Download int64 `json:"download"`
|
||||
}
|
||||
GetUserLoginLogsRequest {
|
||||
Page int `form:"page"`
|
||||
|
||||
+7
-36
@@ -171,13 +171,13 @@ type (
|
||||
DeviceLimit int64 `json:"device_limit"`
|
||||
}
|
||||
VerifyConfig {
|
||||
CaptchaType string `json:"captcha_type"` // local or turnstile
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TurnstileSecret string `json:"turnstile_secret"`
|
||||
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"` // User login captcha
|
||||
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"` // User register captcha
|
||||
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"` // Admin login captcha
|
||||
EnableUserResetPasswordCaptcha bool `json:"enable_user_reset_password_captcha"` // User reset password captcha
|
||||
CaptchaType string `json:"captcha_type"` // local or turnstile
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TurnstileSecret string `json:"turnstile_secret"`
|
||||
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"` // User login captcha
|
||||
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"` // User register captcha
|
||||
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"` // Admin login captcha
|
||||
EnableUserResetPasswordCaptcha bool `json:"enable_user_reset_password_captcha"` // User reset password captcha
|
||||
}
|
||||
NodeConfig {
|
||||
NodeSecret string `json:"node_secret"`
|
||||
@@ -536,35 +536,6 @@ type (
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
UserSubscribeDetail {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User User `json:"user"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
NodeGroupId int64 `json:"node_group_id"`
|
||||
NodeGroupName string `json:"node_group_name"`
|
||||
GroupLocked bool `json:"group_locked"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
SpeedLimit int64 `json:"speed_limit"`
|
||||
TrafficLimit []TrafficLimit `json:"user_traffic_limit"`
|
||||
PlanSpeedLimit int64 `json:"plan_speed_limit"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
EffectiveSpeed int64 `json:"effective_speed"`
|
||||
IsThrottled bool `json:"is_throttled"`
|
||||
ThrottleRule string `json:"throttle_rule,omitempty"`
|
||||
ThrottleStart int64 `json:"throttle_start,omitempty"`
|
||||
ThrottleEnd int64 `json:"throttle_end,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
UserAffiliate {
|
||||
Avatar string `json:"avatar"`
|
||||
Identifier string `json:"identifier"`
|
||||
|
||||
@@ -40,6 +40,8 @@ services:
|
||||
depends_on:
|
||||
tempo:
|
||||
condition: service_started
|
||||
minio:
|
||||
condition: service_healthy
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
@@ -239,6 +241,63 @@ services:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ----------------------------------------------------
|
||||
# 10. MinIO (测试环境 S3 兼容对象存储)
|
||||
# 仅绑定宿主机 127.0.0.1,供 host 网络的 ppanel-server 访问,不对公网开放。
|
||||
# ----------------------------------------------------
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-05-24T17-08-30Z-cpuv1
|
||||
container_name: ppanel-minio
|
||||
restart: always
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: "${MINIO_ROOT_USER:?请在 .env 文件中设置 MINIO_ROOT_USER}"
|
||||
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?请在 .env 文件中设置 MINIO_ROOT_PASSWORD}"
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- ./data/minio:/data
|
||||
ports:
|
||||
- "127.0.0.1:9000:9000"
|
||||
- "127.0.0.1:9001:9001"
|
||||
networks:
|
||||
- ppanel_net
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
minio-init:
|
||||
image: minio/mc:RELEASE.2025-07-21T05-28-08Z-cpuv1
|
||||
container_name: ppanel-minio-init
|
||||
restart: "no"
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MINIO_ROOT_USER: "${MINIO_ROOT_USER:?请在 .env 文件中设置 MINIO_ROOT_USER}"
|
||||
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?请在 .env 文件中设置 MINIO_ROOT_PASSWORD}"
|
||||
MINIO_BUCKET: "${MINIO_BUCKET:?请在 .env 文件中设置 MINIO_BUCKET}"
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
mc alias set local http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}"
|
||||
mc mb --ignore-existing "local/$${MINIO_BUCKET}"
|
||||
networks:
|
||||
- ppanel_net
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
loki_data:
|
||||
grafana_data:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Purpose: Rollback user-level speed limit overrides from user_subscribe
|
||||
|
||||
ALTER TABLE `user_subscribe`
|
||||
DROP COLUMN IF EXISTS `traffic_limit`,
|
||||
DROP COLUMN IF EXISTS `speed_limit`;
|
||||
@@ -1,37 +0,0 @@
|
||||
-- Purpose: Add user-level speed limit overrides to user_subscribe
|
||||
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'user_subscribe'
|
||||
AND COLUMN_NAME = 'speed_limit'
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@column_exists = 0,
|
||||
'ALTER TABLE `user_subscribe` ADD COLUMN `speed_limit` int NOT NULL DEFAULT 0 COMMENT ''User-level speed limit override (Mbps, 0=use plan default)'' AFTER `upload`',
|
||||
'SELECT ''Column speed_limit already exists in user_subscribe table'''
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'user_subscribe'
|
||||
AND COLUMN_NAME = 'traffic_limit'
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@column_exists = 0,
|
||||
'ALTER TABLE `user_subscribe` ADD COLUMN `traffic_limit` text DEFAULT NULL COMMENT ''User-level traffic limit rules override (JSON, NULL=use plan default)'' AFTER `speed_limit`',
|
||||
'SELECT ''Column traffic_limit already exists in user_subscribe table'''
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,7 +0,0 @@
|
||||
ALTER TABLE `order`
|
||||
DROP COLUMN IF EXISTS `promo_discount`,
|
||||
DROP COLUMN IF EXISTS `promo_rule_id`;
|
||||
|
||||
DROP TABLE IF EXISTS `promo_usage`;
|
||||
DROP TABLE IF EXISTS `subscribe_promo`;
|
||||
DROP TABLE IF EXISTS `promo_rule`;
|
||||
@@ -1,52 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS `promo_rule` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '规则名称,如"新客7天优惠"',
|
||||
`type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '规则类型:new_user / inactive_user / campaign',
|
||||
`params` JSON NOT NULL COMMENT '类型专属参数',
|
||||
`priority` INT NOT NULL DEFAULT 0 COMMENT '优先级,数值越大越优先匹配',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`start_time` DATETIME DEFAULT NULL COMMENT '生效开始时间,NULL=立即生效',
|
||||
`end_time` DATETIME DEFAULT NULL COMMENT '生效结束时间,NULL=永不过期',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted_at` DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_enabled_priority` (`enabled`, `priority` DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销规则表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `subscribe_promo` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID',
|
||||
`promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID',
|
||||
`promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_subscribe_rule` (`subscribe_id`, `promo_rule_id`),
|
||||
KEY `idx_promo_rule_id` (`promo_rule_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格促销价表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `promo_usage` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
|
||||
`promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '使用的规则 ID',
|
||||
`subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '购买的规格 ID',
|
||||
`order_no` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '关联订单号',
|
||||
`promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '使用时的促销单价(分)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_rule` (`user_id`, `promo_rule_id`),
|
||||
KEY `idx_order_no` (`order_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销使用记录表';
|
||||
|
||||
SET @promo_rule_id_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'order' AND COLUMN_NAME = 'promo_rule_id');
|
||||
SET @promo_rule_id_sql = IF(@promo_rule_id_exists = 0, 'ALTER TABLE `order` ADD COLUMN `promo_rule_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''促销规则ID, 0=未使用促销'' AFTER `discount`', 'SELECT 1');
|
||||
PREPARE stmt FROM @promo_rule_id_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @promo_discount_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'order' AND COLUMN_NAME = 'promo_discount');
|
||||
SET @promo_discount_sql = IF(@promo_discount_exists = 0, 'ALTER TABLE `order` ADD COLUMN `promo_discount` BIGINT NOT NULL DEFAULT 0 COMMENT ''促销优惠金额(分)'' AFTER `promo_rule_id`', 'SELECT 1');
|
||||
PREPARE stmt FROM @promo_discount_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -22,8 +22,6 @@ type Details struct {
|
||||
Price int64 `gorm:"type:int;not null;default:0;comment:Original price"`
|
||||
Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"`
|
||||
Discount int64 `gorm:"type:int;not null;default:0;comment:Order Discount"`
|
||||
PromoRuleID int64 `gorm:"type:bigint unsigned;not null;default:0;comment:Promo Rule ID, 0 means no promo"`
|
||||
PromoDiscount int64 `gorm:"type:bigint;not null;default:0;comment:Promo Discount Amount"`
|
||||
Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"`
|
||||
CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount"`
|
||||
PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Id"`
|
||||
|
||||
@@ -3,34 +3,32 @@ package order
|
||||
import "time"
|
||||
|
||||
type Order struct {
|
||||
Id int64 `gorm:"primaryKey"`
|
||||
ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"`
|
||||
Id int64 `gorm:"primaryKey"`
|
||||
ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"`
|
||||
UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id"`
|
||||
SubscriptionUserId int64 `gorm:"type:bigint;not null;default:0;comment:Target user ID for subscription (0=same as UserId)"`
|
||||
OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"`
|
||||
Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"`
|
||||
Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"`
|
||||
Price int64 `gorm:"type:int;not null;default:0;comment:Original price"`
|
||||
Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"`
|
||||
GiftAmount int64 `gorm:"type:int;not null;default:0;comment:User Gift Amount"`
|
||||
Discount int64 `gorm:"type:int;not null;default:0;comment:Discount Amount"`
|
||||
PromoRuleID int64 `gorm:"type:bigint unsigned;not null;default:0;comment:Promo Rule ID, 0 means no promo"`
|
||||
PromoDiscount int64 `gorm:"type:bigint;not null;default:0;comment:Promo Discount Amount"`
|
||||
Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"`
|
||||
CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount Amount"`
|
||||
Commission int64 `gorm:"type:int;not null;default:0;comment:Order Commission"`
|
||||
PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Method Id"`
|
||||
Method string `gorm:"type:varchar(255);not null;default:'';comment:Payment Method"`
|
||||
FeeAmount int64 `gorm:"type:int;not null;default:0;comment:Fee Amount"`
|
||||
TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"`
|
||||
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished;"`
|
||||
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"`
|
||||
SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"`
|
||||
AppAccountToken string `gorm:"type:varchar(36);default:null;comment:Apple IAP App Account Token (UUID)"`
|
||||
ActivationContext string `gorm:"type:text;default:null;comment:Activation context JSON (guest/redemption info for DB fallback)"`
|
||||
IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"`
|
||||
Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"`
|
||||
Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"`
|
||||
Price int64 `gorm:"type:int;not null;default:0;comment:Original price"`
|
||||
Amount int64 `gorm:"type:int;not null;default:0;comment:Order Amount"`
|
||||
GiftAmount int64 `gorm:"type:int;not null;default:0;comment:User Gift Amount"`
|
||||
Discount int64 `gorm:"type:int;not null;default:0;comment:Discount Amount"`
|
||||
Coupon string `gorm:"type:varchar(255);default:null;comment:Coupon"`
|
||||
CouponDiscount int64 `gorm:"type:int;not null;default:0;comment:Coupon Discount Amount"`
|
||||
Commission int64 `gorm:"type:int;not null;default:0;comment:Order Commission"`
|
||||
PaymentId int64 `gorm:"type:bigint;not null;default:0;comment:Payment Method Id"`
|
||||
Method string `gorm:"type:varchar(255);not null;default:'';comment:Payment Method"`
|
||||
FeeAmount int64 `gorm:"type:int;not null;default:0;comment:Fee Amount"`
|
||||
TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"`
|
||||
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished;"`
|
||||
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"`
|
||||
SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"`
|
||||
AppAccountToken string `gorm:"type:varchar(36);default:null;comment:Apple IAP App Account Token (UUID)"`
|
||||
ActivationContext string `gorm:"type:text;default:null;comment:Activation context JSON (guest/redemption info for DB fallback)"`
|
||||
IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
}
|
||||
|
||||
type OrdersTotal struct {
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package promo_rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/cache"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var _ Model = (*customPromoRuleModel)(nil)
|
||||
|
||||
var cachePromoRuleIdPrefix = "cache:promo_rule:id:"
|
||||
|
||||
type (
|
||||
Model interface {
|
||||
promoRuleModel
|
||||
customPromoRuleLogicModel
|
||||
}
|
||||
|
||||
promoRuleModel interface {
|
||||
Insert(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error
|
||||
FindOne(ctx context.Context, id int64) (*PromoRule, error)
|
||||
Update(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error
|
||||
Delete(ctx context.Context, id int64, tx ...*gorm.DB) error
|
||||
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
|
||||
}
|
||||
|
||||
customPromoRuleModel struct {
|
||||
*defaultPromoRuleModel
|
||||
}
|
||||
defaultPromoRuleModel struct {
|
||||
cache.CachedConn
|
||||
table string
|
||||
}
|
||||
)
|
||||
|
||||
func newPromoRuleModel(db *gorm.DB, c *redis.Client) *defaultPromoRuleModel {
|
||||
return &defaultPromoRuleModel{
|
||||
CachedConn: cache.NewConn(db, c),
|
||||
table: "`promo_rule`",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultPromoRuleModel) getCacheKeys(data *PromoRule) []string {
|
||||
if data == nil {
|
||||
return []string{}
|
||||
}
|
||||
return []string{fmt.Sprintf("%s%v", cachePromoRuleIdPrefix, data.Id)}
|
||||
}
|
||||
|
||||
func (m *defaultPromoRuleModel) Insert(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error {
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Create(data).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}
|
||||
|
||||
func (m *defaultPromoRuleModel) FindOne(ctx context.Context, id int64) (*PromoRule, error) {
|
||||
key := fmt.Sprintf("%s%v", cachePromoRuleIdPrefix, id)
|
||||
var resp PromoRule
|
||||
err := m.QueryCtx(ctx, &resp, key, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&PromoRule{}).Where("`id` = ?", id).First(v).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (m *defaultPromoRuleModel) Update(ctx context.Context, data *PromoRule, tx ...*gorm.DB) error {
|
||||
old, err := m.FindOne(ctx, data.Id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Save(data).Error
|
||||
}, m.getCacheKeys(old)...)
|
||||
}
|
||||
|
||||
func (m *defaultPromoRuleModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error {
|
||||
data, err := m.FindOne(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Delete(&PromoRule{}, id).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}
|
||||
|
||||
func (m *defaultPromoRuleModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
|
||||
return m.TransactCtx(ctx, fn)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package promo_rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type customPromoRuleLogicModel interface {
|
||||
QueryEnabledByPriority(ctx context.Context) ([]*PromoRule, error)
|
||||
QueryAvailableByPriority(ctx context.Context, now time.Time) ([]*PromoRule, error)
|
||||
}
|
||||
|
||||
func NewModel(conn *gorm.DB, c *redis.Client) Model {
|
||||
return &customPromoRuleModel{
|
||||
defaultPromoRuleModel: newPromoRuleModel(conn, c),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *customPromoRuleModel) QueryEnabledByPriority(ctx context.Context) ([]*PromoRule, error) {
|
||||
var list []*PromoRule
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&PromoRule{}).
|
||||
Where("enabled = ?", true).
|
||||
Order("priority DESC, id DESC").
|
||||
Find(v).Error
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *customPromoRuleModel) QueryAvailableByPriority(ctx context.Context, now time.Time) ([]*PromoRule, error) {
|
||||
var list []*PromoRule
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&PromoRule{}).
|
||||
Where("enabled = ?", true).
|
||||
Where("(start_time IS NULL OR start_time <= ?)", now).
|
||||
Where("(end_time IS NULL OR end_time >= ?)", now).
|
||||
Order("priority DESC, id DESC").
|
||||
Find(v).Error
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package promo_rule
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type JSONRawMessage json.RawMessage
|
||||
|
||||
func (j *JSONRawMessage) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*j = JSONRawMessage([]byte("{}"))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
*j = JSONRawMessage(v)
|
||||
case string:
|
||||
*j = JSONRawMessage([]byte(v))
|
||||
default:
|
||||
*j = JSONRawMessage([]byte("{}"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j JSONRawMessage) Value() (driver.Value, error) {
|
||||
if len(j) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
data, err := json.RawMessage(j).MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (j JSONRawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(j) == 0 {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
return json.RawMessage(j).MarshalJSON()
|
||||
}
|
||||
|
||||
func (j *JSONRawMessage) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
*j = JSONRawMessage([]byte("{}"))
|
||||
return nil
|
||||
}
|
||||
*j = JSONRawMessage(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
type PromoRule struct {
|
||||
Id int64 `gorm:"type:bigint unsigned;primaryKey"`
|
||||
Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Name"`
|
||||
Type string `gorm:"type:varchar(32);not null;default:'';comment:Rule Type: new_user, inactive_user, campaign"`
|
||||
Params JSONRawMessage `gorm:"type:json;not null;comment:Rule Params"`
|
||||
Priority int64 `gorm:"type:int;not null;default:0;index:idx_enabled_priority,priority:2,sort:desc;comment:Priority"`
|
||||
Enabled bool `gorm:"type:tinyint(1);not null;default:1;index:idx_enabled_priority,priority:1;comment:Enabled"`
|
||||
StartTime *time.Time `gorm:"type:datetime;default:null;comment:Start Time"`
|
||||
EndTime *time.Time `gorm:"type:datetime;default:null;comment:End Time"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:Delete Time"`
|
||||
}
|
||||
|
||||
func (PromoRule) TableName() string {
|
||||
return "promo_rule"
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package promo_usage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/cache"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var _ Model = (*customPromoUsageModel)(nil)
|
||||
|
||||
var cachePromoUsageIdPrefix = "cache:promo_usage:id:"
|
||||
|
||||
type (
|
||||
Model interface {
|
||||
promoUsageModel
|
||||
customPromoUsageLogicModel
|
||||
}
|
||||
|
||||
promoUsageModel interface {
|
||||
Insert(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error
|
||||
FindOne(ctx context.Context, id int64) (*PromoUsage, error)
|
||||
Update(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error
|
||||
Delete(ctx context.Context, id int64, tx ...*gorm.DB) error
|
||||
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
|
||||
}
|
||||
|
||||
customPromoUsageModel struct {
|
||||
*defaultPromoUsageModel
|
||||
}
|
||||
defaultPromoUsageModel struct {
|
||||
cache.CachedConn
|
||||
table string
|
||||
}
|
||||
)
|
||||
|
||||
func newPromoUsageModel(db *gorm.DB, c *redis.Client) *defaultPromoUsageModel {
|
||||
return &defaultPromoUsageModel{
|
||||
CachedConn: cache.NewConn(db, c),
|
||||
table: "`promo_usage`",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultPromoUsageModel) getCacheKeys(data *PromoUsage) []string {
|
||||
if data == nil {
|
||||
return []string{}
|
||||
}
|
||||
return []string{fmt.Sprintf("%s%v", cachePromoUsageIdPrefix, data.Id)}
|
||||
}
|
||||
|
||||
func (m *defaultPromoUsageModel) Insert(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error {
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Create(data).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}
|
||||
|
||||
func (m *defaultPromoUsageModel) FindOne(ctx context.Context, id int64) (*PromoUsage, error) {
|
||||
key := fmt.Sprintf("%s%v", cachePromoUsageIdPrefix, id)
|
||||
var resp PromoUsage
|
||||
err := m.QueryCtx(ctx, &resp, key, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&PromoUsage{}).Where("`id` = ?", id).First(v).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (m *defaultPromoUsageModel) Update(ctx context.Context, data *PromoUsage, tx ...*gorm.DB) error {
|
||||
old, err := m.FindOne(ctx, data.Id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Save(data).Error
|
||||
}, m.getCacheKeys(old)...)
|
||||
}
|
||||
|
||||
func (m *defaultPromoUsageModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error {
|
||||
data, err := m.FindOne(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Delete(&PromoUsage{}, id).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}
|
||||
|
||||
func (m *defaultPromoUsageModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
|
||||
return m.TransactCtx(ctx, fn)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package promo_usage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type customPromoUsageLogicModel interface {
|
||||
FindByOrderNo(ctx context.Context, orderNo string) ([]*PromoUsage, error)
|
||||
FindByUserAndRule(ctx context.Context, userId, promoRuleId int64) ([]*PromoUsage, error)
|
||||
QueryListByPage(ctx context.Context, page, size int, userId, promoRuleId, subscribeId int64) (int64, []*PromoUsage, error)
|
||||
}
|
||||
|
||||
func NewModel(conn *gorm.DB, c *redis.Client) Model {
|
||||
return &customPromoUsageModel{
|
||||
defaultPromoUsageModel: newPromoUsageModel(conn, c),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *customPromoUsageModel) FindByOrderNo(ctx context.Context, orderNo string) ([]*PromoUsage, error) {
|
||||
var list []*PromoUsage
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&PromoUsage{}).
|
||||
Where("order_no = ?", orderNo).
|
||||
Order("id DESC").
|
||||
Find(v).Error
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *customPromoUsageModel) FindByUserAndRule(ctx context.Context, userId, promoRuleId int64) ([]*PromoUsage, error) {
|
||||
var list []*PromoUsage
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&PromoUsage{}).
|
||||
Where("user_id = ? AND promo_rule_id = ?", userId, promoRuleId).
|
||||
Order("id DESC").
|
||||
Find(v).Error
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *customPromoUsageModel) QueryListByPage(ctx context.Context, page, size int, userId, promoRuleId, subscribeId int64) (int64, []*PromoUsage, error) {
|
||||
var list []*PromoUsage
|
||||
var total int64
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
db := conn.Model(&PromoUsage{})
|
||||
if userId > 0 {
|
||||
db = db.Where("user_id = ?", userId)
|
||||
}
|
||||
if promoRuleId > 0 {
|
||||
db = db.Where("promo_rule_id = ?", promoRuleId)
|
||||
}
|
||||
if subscribeId > 0 {
|
||||
db = db.Where("subscribe_id = ?", subscribeId)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(v).Error
|
||||
})
|
||||
return total, list, err
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package promo_usage
|
||||
|
||||
import "time"
|
||||
|
||||
type PromoUsage struct {
|
||||
Id int64 `gorm:"type:bigint unsigned;primaryKey"`
|
||||
UserId int64 `gorm:"type:bigint unsigned;not null;index:idx_user_rule,priority:1;comment:User Id"`
|
||||
PromoRuleId int64 `gorm:"type:bigint unsigned;not null;index:idx_user_rule,priority:2;comment:Promo Rule Id"`
|
||||
SubscribeId int64 `gorm:"type:bigint unsigned;not null;comment:Subscribe Id"`
|
||||
OrderNo string `gorm:"type:varchar(255);not null;default:'';index:idx_order_no;comment:Order No"`
|
||||
PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
}
|
||||
|
||||
func (PromoUsage) TableName() string {
|
||||
return "promo_usage"
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package subscribe_promo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/cache"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var _ Model = (*customSubscribePromoModel)(nil)
|
||||
|
||||
var cacheSubscribePromoIdPrefix = "cache:subscribe_promo:id:"
|
||||
|
||||
type (
|
||||
Model interface {
|
||||
subscribePromoModel
|
||||
customSubscribePromoLogicModel
|
||||
}
|
||||
|
||||
subscribePromoModel interface {
|
||||
Insert(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error
|
||||
FindOne(ctx context.Context, id int64) (*SubscribePromo, error)
|
||||
Update(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error
|
||||
Delete(ctx context.Context, id int64, tx ...*gorm.DB) error
|
||||
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
|
||||
}
|
||||
|
||||
customSubscribePromoModel struct {
|
||||
*defaultSubscribePromoModel
|
||||
}
|
||||
defaultSubscribePromoModel struct {
|
||||
cache.CachedConn
|
||||
table string
|
||||
}
|
||||
)
|
||||
|
||||
func newSubscribePromoModel(db *gorm.DB, c *redis.Client) *defaultSubscribePromoModel {
|
||||
return &defaultSubscribePromoModel{
|
||||
CachedConn: cache.NewConn(db, c),
|
||||
table: "`subscribe_promo`",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultSubscribePromoModel) getCacheKeys(data *SubscribePromo) []string {
|
||||
if data == nil {
|
||||
return []string{}
|
||||
}
|
||||
return []string{fmt.Sprintf("%s%v", cacheSubscribePromoIdPrefix, data.Id)}
|
||||
}
|
||||
|
||||
func (m *defaultSubscribePromoModel) Insert(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error {
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Create(data).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}
|
||||
|
||||
func (m *defaultSubscribePromoModel) FindOne(ctx context.Context, id int64) (*SubscribePromo, error) {
|
||||
key := fmt.Sprintf("%s%v", cacheSubscribePromoIdPrefix, id)
|
||||
var resp SubscribePromo
|
||||
err := m.QueryCtx(ctx, &resp, key, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&SubscribePromo{}).Where("`id` = ?", id).First(v).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (m *defaultSubscribePromoModel) Update(ctx context.Context, data *SubscribePromo, tx ...*gorm.DB) error {
|
||||
old, err := m.FindOne(ctx, data.Id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Save(data).Error
|
||||
}, m.getCacheKeys(old)...)
|
||||
}
|
||||
|
||||
func (m *defaultSubscribePromoModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error {
|
||||
data, err := m.FindOne(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Delete(&SubscribePromo{}, id).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}
|
||||
|
||||
func (m *defaultSubscribePromoModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
|
||||
return m.TransactCtx(ctx, fn)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package subscribe_promo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/promo_rule"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RulePromo struct {
|
||||
Id int64
|
||||
SubscribeId int64
|
||||
PromoRuleId int64
|
||||
PromoPrice int64
|
||||
RuleName string
|
||||
RuleType string
|
||||
Params promo_rule.JSONRawMessage
|
||||
Priority int64
|
||||
StartTime *time.Time
|
||||
EndTime *time.Time
|
||||
}
|
||||
|
||||
type customSubscribePromoLogicModel interface {
|
||||
FindOneBySubscribeAndRule(ctx context.Context, subscribeId, promoRuleId int64) (*SubscribePromo, error)
|
||||
FindBySubscribeId(ctx context.Context, subscribeId int64) ([]*SubscribePromo, error)
|
||||
FindByPromoRuleId(ctx context.Context, promoRuleId int64) ([]*SubscribePromo, error)
|
||||
QueryEnabledBySubscribeId(ctx context.Context, subscribeId int64) ([]*RulePromo, error)
|
||||
}
|
||||
|
||||
func NewModel(conn *gorm.DB, c *redis.Client) Model {
|
||||
return &customSubscribePromoModel{
|
||||
defaultSubscribePromoModel: newSubscribePromoModel(conn, c),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *customSubscribePromoModel) FindOneBySubscribeAndRule(ctx context.Context, subscribeId, promoRuleId int64) (*SubscribePromo, error) {
|
||||
var resp SubscribePromo
|
||||
err := m.QueryNoCacheCtx(ctx, &resp, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&SubscribePromo{}).
|
||||
Where("subscribe_id = ? AND promo_rule_id = ?", subscribeId, promoRuleId).
|
||||
First(v).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (m *customSubscribePromoModel) FindBySubscribeId(ctx context.Context, subscribeId int64) ([]*SubscribePromo, error) {
|
||||
var list []*SubscribePromo
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&SubscribePromo{}).
|
||||
Where("subscribe_id = ?", subscribeId).
|
||||
Order("id DESC").
|
||||
Find(v).Error
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *customSubscribePromoModel) FindByPromoRuleId(ctx context.Context, promoRuleId int64) ([]*SubscribePromo, error) {
|
||||
var list []*SubscribePromo
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&SubscribePromo{}).
|
||||
Where("promo_rule_id = ?", promoRuleId).
|
||||
Order("id DESC").
|
||||
Find(v).Error
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *customSubscribePromoModel) QueryEnabledBySubscribeId(ctx context.Context, subscribeId int64) ([]*RulePromo, error) {
|
||||
var list []*RulePromo
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Table("subscribe_promo AS sp").
|
||||
Select("sp.id, sp.subscribe_id, sp.promo_rule_id, sp.promo_price, pr.name AS rule_name, pr.type AS rule_type, pr.params, pr.priority, pr.start_time, pr.end_time").
|
||||
Joins("JOIN promo_rule AS pr ON pr.id = sp.promo_rule_id AND pr.deleted_at IS NULL").
|
||||
Where("sp.subscribe_id = ? AND pr.enabled = ?", subscribeId, true).
|
||||
Order("pr.priority DESC, pr.id DESC").
|
||||
Scan(v).Error
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package subscribe_promo
|
||||
|
||||
import "time"
|
||||
|
||||
type SubscribePromo struct {
|
||||
Id int64 `gorm:"type:bigint unsigned;primaryKey"`
|
||||
SubscribeId int64 `gorm:"type:bigint unsigned;not null;index:uk_subscribe_rule,unique,priority:1;comment:Subscribe Id"`
|
||||
PromoRuleId int64 `gorm:"type:bigint unsigned;not null;index:uk_subscribe_rule,unique,priority:2;index:idx_promo_rule_id;comment:Promo Rule Id"`
|
||||
PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
}
|
||||
|
||||
func (SubscribePromo) TableName() string {
|
||||
return "subscribe_promo"
|
||||
}
|
||||
@@ -101,8 +101,6 @@ type Subscribe struct {
|
||||
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
||||
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
||||
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
|
||||
SpeedLimit int64 `gorm:"default:0;comment:User-level speed limit override (Mbps, 0=use plan default)"`
|
||||
TrafficLimit string `gorm:"type:text;default:null;comment:User-level traffic limit rules override (JSON)"`
|
||||
ExpiredDownload int64 `gorm:"default:0;comment:Expired period download traffic (bytes)"`
|
||||
ExpiredUpload int64 `gorm:"default:0;comment:Expired period upload traffic (bytes)"`
|
||||
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
||||
|
||||
@@ -21,10 +21,7 @@ import (
|
||||
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
|
||||
"github.com/perfect-panel/server/internal/model/order"
|
||||
"github.com/perfect-panel/server/internal/model/payment"
|
||||
"github.com/perfect-panel/server/internal/model/promo_rule"
|
||||
"github.com/perfect-panel/server/internal/model/promo_usage"
|
||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||
"github.com/perfect-panel/server/internal/model/subscribe_promo"
|
||||
"github.com/perfect-panel/server/internal/model/system"
|
||||
"github.com/perfect-panel/server/internal/model/ticket"
|
||||
"github.com/perfect-panel/server/internal/model/traffic"
|
||||
@@ -48,7 +45,7 @@ type ServiceContext struct {
|
||||
ExchangeRate float64
|
||||
GeoIP *IPLocation
|
||||
SignatureValidator *signature.Validator
|
||||
S3Store *storage.S3Store
|
||||
S3Store *storage.S3Store
|
||||
|
||||
//NodeCache *cache.NodeCacheClient
|
||||
AuthModel auth.Model
|
||||
@@ -68,9 +65,6 @@ type ServiceContext struct {
|
||||
PaymentModel payment.Model
|
||||
DocumentModel document.Model
|
||||
SubscribeModel subscribe.Model
|
||||
PromoRuleModel promo_rule.Model
|
||||
SubscribePromoModel subscribe_promo.Model
|
||||
PromoUsageModel promo_usage.Model
|
||||
TrafficLogModel traffic.Model
|
||||
AnnouncementModel announcement.Model
|
||||
IAPAppleTransactionModel iapapple.Model
|
||||
@@ -146,9 +140,6 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
PaymentModel: payment.NewModel(db, rds),
|
||||
DocumentModel: document.NewModel(db, rds),
|
||||
SubscribeModel: subscribe.NewModel(db, rds),
|
||||
PromoRuleModel: promo_rule.NewModel(db, rds),
|
||||
SubscribePromoModel: subscribe_promo.NewModel(db, rds),
|
||||
PromoUsageModel: promo_usage.NewModel(db, rds),
|
||||
TrafficLogModel: traffic.NewModel(db),
|
||||
AnnouncementModel: announcement.NewModel(db, rds),
|
||||
}
|
||||
|
||||
+34
-41
@@ -541,12 +541,10 @@ type CreateUserRequest struct {
|
||||
}
|
||||
|
||||
type CreateUserSubscribeRequest struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
SpeedLimit int64 `json:"speed_limit,optional"`
|
||||
TrafficLimit string `json:"traffic_limit,optional"`
|
||||
UserId int64 `json:"user_id"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
}
|
||||
|
||||
type CreateUserTicketFollowRequest struct {
|
||||
@@ -3319,14 +3317,12 @@ type UpdateUserSubscribeNoteRequest struct {
|
||||
}
|
||||
|
||||
type UpdateUserSubscribeRequest struct {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Upload int64 `json:"upload"`
|
||||
Download int64 `json:"download"`
|
||||
SpeedLimit *int64 `json:"speed_limit,omitempty"`
|
||||
TrafficLimit *string `json:"traffic_limit,omitempty"`
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
Upload int64 `json:"upload"`
|
||||
Download int64 `json:"download"`
|
||||
}
|
||||
|
||||
type UpdateUserTicketStatusRequest struct {
|
||||
@@ -3478,33 +3474,30 @@ type UserSubscribe struct {
|
||||
}
|
||||
|
||||
type UserSubscribeDetail struct {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User User `json:"user"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
NodeGroupId int64 `json:"node_group_id"`
|
||||
NodeGroupName string `json:"node_group_name"`
|
||||
GroupLocked bool `json:"group_locked"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
SpeedLimit int64 `json:"speed_limit"`
|
||||
TrafficLimit []TrafficLimit `json:"user_traffic_limit"`
|
||||
PlanSpeedLimit int64 `json:"plan_speed_limit"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
EffectiveSpeed int64 `json:"effective_speed"`
|
||||
IsThrottled bool `json:"is_throttled"`
|
||||
ThrottleRule string `json:"throttle_rule,omitempty"`
|
||||
ThrottleStart int64 `json:"throttle_start,omitempty"`
|
||||
ThrottleEnd int64 `json:"throttle_end,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User User `json:"user"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
NodeGroupId int64 `json:"node_group_id"`
|
||||
NodeGroupName string `json:"node_group_name"`
|
||||
GroupLocked bool `json:"group_locked"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
EffectiveSpeed int64 `json:"effective_speed"`
|
||||
IsThrottled bool `json:"is_throttled"`
|
||||
ThrottleRule string `json:"throttle_rule,omitempty"`
|
||||
ThrottleStart int64 `json:"throttle_start,omitempty"`
|
||||
ThrottleEnd int64 `json:"throttle_end,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserSubscribeInfo struct {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# HiFast 上传链路运维 Runbook
|
||||
|
||||
## 适用范围
|
||||
|
||||
本文档用于测试环境 `https://tapi.hifast.biz/v1/public/file/upload` 上传链路自检和 MinIO sidecar 运维。测试环境的对象存储应自包含在 compose 栈内,不依赖外部 `107.173.50.22:5017`。
|
||||
|
||||
## 当前测试环境配置
|
||||
|
||||
- Compose 文件:`/root/bindbox/docker-compose.cloud.yml`
|
||||
- 应用配置:`/root/bindbox/configs/ppanel.yaml`
|
||||
- MinIO 数据目录:`/root/bindbox/data/minio`
|
||||
- MinIO API:`http://127.0.0.1:9000`
|
||||
- MinIO Console:`http://127.0.0.1:9001`,仅绑定本机,需 SSH 隧道访问
|
||||
- Bucket:与 `ppanel.yaml` 的 `S3.Bucket` 保持一致,当前为 `hifastvpn`
|
||||
|
||||
`ppanel-server` 使用 host 网络,所以 `S3.Endpoint` 必须写宿主机 loopback:
|
||||
|
||||
```yaml
|
||||
S3:
|
||||
Enable: true
|
||||
Endpoint: "http://127.0.0.1:9000"
|
||||
Bucket: "hifastvpn"
|
||||
UsePathStyle: true
|
||||
```
|
||||
|
||||
## 启停与状态检查
|
||||
|
||||
```bash
|
||||
cd /root/bindbox
|
||||
docker compose -f docker-compose.cloud.yml up -d minio minio-init
|
||||
docker restart ppanel-server
|
||||
|
||||
docker compose -f docker-compose.cloud.yml ps minio minio-init ppanel-server
|
||||
curl -fsS http://127.0.0.1:9000/minio/health/live
|
||||
curl -fsS https://tapi.hifast.biz/v1/common/heartbeat
|
||||
```
|
||||
|
||||
`minio-init` 是一次性 bucket 初始化容器,正常状态是 `exited` 且退出码为 `0`:
|
||||
|
||||
```bash
|
||||
docker inspect ppanel-minio-init --format 'status={{.State.Status}} exit={{.State.ExitCode}}'
|
||||
docker logs --tail 50 ppanel-minio-init
|
||||
```
|
||||
|
||||
## Bucket 重建
|
||||
|
||||
如 bucket 被误删或新环境首次初始化失败:
|
||||
|
||||
```bash
|
||||
cd /root/bindbox
|
||||
docker compose -f docker-compose.cloud.yml up -d minio
|
||||
docker compose -f docker-compose.cloud.yml run --rm minio-init
|
||||
```
|
||||
|
||||
确认 bucket:
|
||||
|
||||
```bash
|
||||
docker logs --tail 50 ppanel-minio-init
|
||||
find /root/bindbox/data/minio -maxdepth 3 -type d | sort | head
|
||||
```
|
||||
|
||||
## 上传链路自检
|
||||
|
||||
基础健康检查:
|
||||
|
||||
```bash
|
||||
curl -i https://tapi.hifast.biz/v1/common/heartbeat
|
||||
```
|
||||
|
||||
完整上传需要有效用户 JWT。复测标准:
|
||||
|
||||
- `POST /v1/public/file/upload` 返回 HTTP 200
|
||||
- 响应业务 `code=200`
|
||||
- `data.url` 指向 `S3.PublicBaseURL + object_key`
|
||||
- MinIO 数据目录可看到对应对象
|
||||
- `ppanel-server` 日志没有 `put object failed` 或 `context canceled`
|
||||
|
||||
排查命令:
|
||||
|
||||
```bash
|
||||
docker logs --since 10m ppanel-server | grep -Ei 'file/upload|put object|context canceled|s3'
|
||||
find /root/bindbox/data/minio/hifastvpn -type f | sort | tail
|
||||
```
|
||||
|
||||
## 备份
|
||||
|
||||
变更前必须备份:
|
||||
|
||||
```bash
|
||||
cd /root/bindbox
|
||||
ts=$(date +%Y%m%d%H%M%S)
|
||||
cp docker-compose.cloud.yml docker-compose.cloud.yml.bak.hif-101.$ts
|
||||
cp .env .env.bak.hif-101.$ts
|
||||
cp configs/ppanel.yaml configs/ppanel.yaml.bak.$ts
|
||||
```
|
||||
|
||||
MinIO 数据备份:
|
||||
|
||||
```bash
|
||||
cd /root/bindbox
|
||||
tar -czf minio-data.$(date +%Y%m%d%H%M%S).tar.gz data/minio
|
||||
```
|
||||
|
||||
## 回滚
|
||||
|
||||
恢复外部 S3 配置或回到变更前状态:
|
||||
|
||||
```bash
|
||||
cd /root/bindbox
|
||||
cp configs/ppanel.yaml.bak.YYYYMMDDHHMMSS configs/ppanel.yaml
|
||||
cp docker-compose.cloud.yml.bak.hif-101.YYYYMMDDHHMMSS docker-compose.cloud.yml
|
||||
cp .env.bak.hif-101.YYYYMMDDHHMMSS .env
|
||||
docker compose -f docker-compose.cloud.yml stop minio
|
||||
docker restart ppanel-server
|
||||
```
|
||||
|
||||
保留 `/root/bindbox/data/minio` 便于复盘,不要在回滚时删除数据目录。
|
||||
|
||||
## 旧 Endpoint 清理
|
||||
|
||||
测试栈活动配置不得继续引用旧外部 endpoint:
|
||||
|
||||
```bash
|
||||
cd /root/bindbox
|
||||
grep -R '107\.173\.50\.22:5017' -n docker-compose.cloud.yml configs/ppanel.yaml .env || true
|
||||
```
|
||||
Reference in New Issue
Block a user