Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions controller/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ var auditContentTemplates = map[string]string{

"redemption.create": "Created ${count} redemption codes named ${name} (${quota} each)",

"subscription.plan_reset": "Reset active subscriptions for plan ${plan_id}",
"subscription.user_plan_reset": "Reset active plan ${plan_id} subscriptions for user ${target_user_id}",
"subscription.plan_reset": "Reset active subscriptions for plan ${plan_id}",
"subscription.user_plan_reset": "Reset active plan ${plan_id} subscriptions for user ${target_user_id}",
"subscription.admin_grant": "Granted subscription plan ${plan_title} (ID: ${plan_id}) to user ${target_user_id} (mode: ${mode})",
"subscription.admin_grant_batch": "Granted subscription plan ${plan_title} (ID: ${plan_id}) to ${count} users (mode: ${mode})",
"subscription.granted": "Admin granted you subscription plan ${plan_title} (ID: ${plan_id})",
}

// auditContentEN 按 action 模板渲染英文兜底文本;未登记的 action 退回 action 本身。
Expand Down
86 changes: 81 additions & 5 deletions controller/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,10 @@ func AdminUpdateSubscriptionPlanStatus(c *gin.Context) {
}

type AdminBindSubscriptionRequest struct {
UserId int `json:"user_id"`
PlanId int `json:"plan_id"`
UserId int `json:"user_id"`
PlanId int `json:"plan_id"`
Mode string `json:"mode"`
EndTime int64 `json:"end_time"`
}

func AdminBindSubscription(c *gin.Context) {
Expand All @@ -363,11 +365,13 @@ func AdminBindSubscription(c *gin.Context) {
common.ApiErrorMsg(c, "参数错误")
return
}
msg, err := model.AdminBindSubscription(req.UserId, req.PlanId, "")
opts := model.AdminGrantOptions{Mode: req.Mode, EndTime: req.EndTime}
msg, effectiveMode, err := model.AdminBindSubscription(req.UserId, req.PlanId, opts)
if err != nil {
common.ApiError(c, err)
return
}
recordSubscriptionGrantLogs(c, req.UserId, req.PlanId, opts, effectiveMode)
if msg != "" {
common.ApiSuccess(c, gin.H{"message": msg})
return
Expand All @@ -392,7 +396,16 @@ func AdminListUserSubscriptions(c *gin.Context) {
}

type AdminCreateUserSubscriptionRequest struct {
PlanId int `json:"plan_id"`
PlanId int `json:"plan_id"`
Mode string `json:"mode"`
EndTime int64 `json:"end_time"`
}

type AdminBindSubscriptionBatchRequest struct {
UserIds []int `json:"user_ids"`
PlanId int `json:"plan_id"`
Mode string `json:"mode"`
EndTime int64 `json:"end_time"`
}

type AdminResetSubscriptionRequest struct {
Expand All @@ -417,6 +430,67 @@ func recordSubscriptionResetUserLogs(result *model.SubscriptionResetResult, admi
}
}

// recordSubscriptionGrantLogs records both the target user's manage log and the
// operator audit entry after an admin grants a subscription without payment.
// effectiveMode is the mode that actually ran (a renew with nothing to extend
// falls back to create), so audit history reflects the applied behaviour.
func recordSubscriptionGrantLogs(c *gin.Context, userId int, planId int, opts model.AdminGrantOptions, effectiveMode string) {
planTitle := ""
if plan, err := model.GetSubscriptionPlanById(planId); err == nil && plan != nil {
planTitle = plan.Title
}
// The target user's manage log stores a language-neutral op descriptor so
// the frontend can localize it per viewer, like the other audit logs.
userParams := map[string]interface{}{"plan_title": planTitle, "plan_id": planId}
model.RecordOperationAuditLog(userId, auditContentEN("subscription.granted", userParams), "",
"subscription.granted", userParams, auditOperatorInfo(c), nil)
recordManageAuditFor(c, userId, "subscription.admin_grant", map[string]interface{}{
"target_user_id": userId,
"plan_id": planId,
"plan_title": planTitle,
"mode": effectiveMode,
"end_time": opts.EndTime,
})
}

// AdminBindSubscriptionBatch grants one plan to several users at once.
func AdminBindSubscriptionBatch(c *gin.Context) {
if !requirePaymentCompliance(c) {
return
}

var req AdminBindSubscriptionBatchRequest
if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 || len(req.UserIds) == 0 {
common.ApiErrorMsg(c, "参数错误")
return
}
opts := model.AdminGrantOptions{Mode: req.Mode, EndTime: req.EndTime}
result, err := model.AdminBindSubscriptionBatch(req.UserIds, req.PlanId, opts)
if err != nil {
common.ApiError(c, err)
return
}
adminInfo := auditOperatorInfo(c)
userParams := map[string]interface{}{"plan_title": result.PlanTitle, "plan_id": result.PlanId}
content := auditContentEN("subscription.granted", userParams)
for _, userId := range result.SucceededUsers {
model.RecordOperationAuditLog(userId, content, "", "subscription.granted", userParams, adminInfo, nil)
}
// The batch call already rejected invalid modes, so the error is impossible here.
normalizedMode, _ := model.NormalizeGrantMode(req.Mode)
recordManageAudit(c, "subscription.admin_grant_batch", map[string]interface{}{
"plan_id": result.PlanId,
"plan_title": result.PlanTitle,
"mode": normalizedMode,
"end_time": req.EndTime,
"user_ids": result.SucceededUsers,
"count": result.SuccessCount,
"success_count": result.SuccessCount,
"failed_count": result.FailedCount,
})
common.ApiSuccess(c, result)
}

// AdminCreateUserSubscription creates a new user subscription from a plan (no payment).
func AdminCreateUserSubscription(c *gin.Context) {
if !requirePaymentCompliance(c) {
Expand All @@ -433,11 +507,13 @@ func AdminCreateUserSubscription(c *gin.Context) {
common.ApiErrorMsg(c, "参数错误")
return
}
msg, err := model.AdminBindSubscription(userId, req.PlanId, "")
opts := model.AdminGrantOptions{Mode: req.Mode, EndTime: req.EndTime}
msg, effectiveMode, err := model.AdminBindSubscription(userId, req.PlanId, opts)
if err != nil {
common.ApiError(c, err)
return
}
recordSubscriptionGrantLogs(c, userId, req.PlanId, opts, effectiveMode)
if msg != "" {
common.ApiSuccess(c, gin.H{"message": msg})
return
Expand Down
19 changes: 15 additions & 4 deletions model/db_time.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
package model

import "github.com/QuantumNous/new-api/common"
import (
"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
)

// GetDBTimestamp returns a UNIX timestamp from database time.
// Falls back to application time on error.
func GetDBTimestamp() int64 {
return getDBTimestampFrom(DB)
}

// getDBTimestampFrom reads database time through the given handle. Callers
// inside a transaction must pass their tx: going through the global DB would
// check out a second connection from the pool for the duration of the
// transaction.
func getDBTimestampFrom(db *gorm.DB) int64 {
var ts int64
var err error
switch {
case common.UsingMainDatabase(common.DatabaseTypePostgreSQL):
err = DB.Raw("SELECT EXTRACT(EPOCH FROM NOW())::bigint").Scan(&ts).Error
err = db.Raw("SELECT EXTRACT(EPOCH FROM NOW())::bigint").Scan(&ts).Error
case common.UsingMainDatabase(common.DatabaseTypeSQLite):
err = DB.Raw("SELECT strftime('%s','now')").Scan(&ts).Error
err = db.Raw("SELECT strftime('%s','now')").Scan(&ts).Error
default:
err = DB.Raw("SELECT UNIX_TIMESTAMP()").Scan(&ts).Error
err = db.Raw("SELECT UNIX_TIMESTAMP()").Scan(&ts).Error
}
if err != nil || ts <= 0 {
return common.GetTimestamp()
Expand Down
Loading