Files
ttstd-infinite-canvas/backend/internal/app/credit_policy.go

169 lines
5.7 KiB
Go

package app
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
)
const creditPolicySettingKey = "credit_policy"
type CreditPolicy struct {
SignupBonusMicrocredits int64 `json:"signupBonusMicrocredits"`
CheckinBonusMicrocredits int64 `json:"checkinBonusMicrocredits"`
DefaultMultiplierBPS int64 `json:"defaultMultiplierBasisPoints"`
ModelMultiplierBPS map[string]int64 `json:"modelMultiplierBasisPoints"`
}
type PublicCreditPolicy struct {
SignupBonusMicrocredits int64 `json:"signupBonusMicrocredits"`
CheckinBonusMicrocredits int64 `json:"checkinBonusMicrocredits"`
CheckedInToday bool `json:"checkedInToday"`
}
func defaultCreditPolicy() CreditPolicy {
return CreditPolicy{SignupBonusMicrocredits: 100 * CreditScale, CheckinBonusMicrocredits: 10 * CreditScale, DefaultMultiplierBPS: 10_000, ModelMultiplierBPS: map[string]int64{}}
}
func validateCreditPolicy(policy CreditPolicy) error {
if policy.SignupBonusMicrocredits < 0 || policy.CheckinBonusMicrocredits < 0 {
return BadAuthRequest("注册和签到奖励不能小于 0")
}
if policy.SignupBonusMicrocredits > 1_000_000*CreditScale || policy.CheckinBonusMicrocredits > 100_000*CreditScale {
return BadAuthRequest("积分奖励超出允许范围")
}
if policy.DefaultMultiplierBPS <= 0 || policy.DefaultMultiplierBPS > 1_000_000 {
return BadAuthRequest("默认模型倍率必须在 0.0001-100 之间")
}
for modelKey, multiplier := range policy.ModelMultiplierBPS {
if strings.TrimSpace(modelKey) == "" || multiplier <= 0 || multiplier > 1_000_000 {
return BadAuthRequest("模型倍率配置无效")
}
}
return nil
}
func (s *Service) creditPolicy() (CreditPolicy, error) {
setting, err := s.repo.SystemSetting(creditPolicySettingKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
return defaultCreditPolicy(), nil
}
if err != nil {
return CreditPolicy{}, err
}
var policy CreditPolicy
if json.Unmarshal([]byte(setting.ValueJSON), &policy) != nil {
return CreditPolicy{}, errors.New("积分策略配置格式无效")
}
if policy.ModelMultiplierBPS == nil {
policy.ModelMultiplierBPS = map[string]int64{}
}
if err := validateCreditPolicy(policy); err != nil {
return CreditPolicy{}, err
}
return policy, nil
}
func (s *Service) AdminCreditPolicy(actor *model.User) (CreditPolicy, error) {
if err := s.RequireAdmin(actor); err != nil {
return CreditPolicy{}, err
}
return s.creditPolicy()
}
func (s *Service) UpdateCreditPolicy(actor *model.User, policy CreditPolicy) (CreditPolicy, error) {
if err := s.RequireAdmin(actor); err != nil {
return CreditPolicy{}, err
}
if policy.ModelMultiplierBPS == nil {
policy.ModelMultiplierBPS = map[string]int64{}
}
if err := validateCreditPolicy(policy); err != nil {
return CreditPolicy{}, err
}
encoded, err := json.Marshal(policy)
if err != nil {
return CreditPolicy{}, err
}
setting := model.SystemSetting{Key: creditPolicySettingKey, ValueJSON: string(encoded), UpdatedBy: actor.ID}
current, err := s.repo.SystemSetting(creditPolicySettingKey)
if err == nil {
setting.CreatedAt = current.CreatedAt
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return CreditPolicy{}, err
}
if err := s.repo.SaveSystemSetting(&setting); err != nil {
return CreditPolicy{}, err
}
if err := s.appendAdminAudit(actor, "credit_policy.update", "system_setting", creditPolicySettingKey, "更新积分策略", policy); err != nil {
return CreditPolicy{}, err
}
return policy, nil
}
func (s *Service) ensureSignupBonus(userID string) error {
enabled, err := s.FeatureEnabled(FeatureCredits)
if err != nil || !enabled {
return err
}
policy, err := s.creditPolicy()
if err != nil || policy.SignupBonusMicrocredits == 0 {
return err
}
_, _, err = s.repo.GrantCreditsOnce(userID, model.CreditLedgerSignupBonus, policy.SignupBonusMicrocredits, "signup:"+userID, "新用户默认积分")
return err
}
func (s *Service) CheckinCredits(user *model.User) (*model.CreditAccount, bool, error) {
if user == nil {
return nil, false, Unauthorized("请先登录")
}
if err := s.RequireFeature(FeatureCredits); err != nil {
return nil, false, err
}
policy, err := s.creditPolicy()
if err != nil {
return nil, false, err
}
if policy.CheckinBonusMicrocredits == 0 {
return nil, false, BadAuthRequest("当前未开启签到奖励")
}
day := time.Now().UTC().Format("2006-01-02")
return s.repo.GrantCreditsOnce(user.ID, model.CreditLedgerCheckinBonus, policy.CheckinBonusMicrocredits, "checkin:"+user.ID+":"+day, "每日签到奖励")
}
func (s *Service) publicCreditPolicy(userID string) (PublicCreditPolicy, error) {
policy, err := s.creditPolicy()
if err != nil {
return PublicCreditPolicy{}, err
}
reference := "checkin:" + userID + ":" + time.Now().UTC().Format("2006-01-02")
checked, err := s.repo.CreditLedgerReferenceExists(reference)
return PublicCreditPolicy{SignupBonusMicrocredits: policy.SignupBonusMicrocredits, CheckinBonusMicrocredits: policy.CheckinBonusMicrocredits, CheckedInToday: checked}, err
}
// 单价、数量和倍率全程使用整数并向上取整,避免浮点误差造成少扣积分。
func creditAmount(unitPrice int64, quantity int64, multiplierBPS int64) (int64, error) {
if unitPrice < 0 || quantity <= 0 || multiplierBPS <= 0 {
return 0, errors.New("积分计费参数无效")
}
if unitPrice > (1<<63-1)/quantity {
return 0, errors.New("积分计费金额溢出")
}
base := unitPrice * quantity
if base > ((1<<63-1)-9_999)/multiplierBPS {
return 0, errors.New("积分计费金额溢出")
}
amount := (base*multiplierBPS + 9_999) / 10_000
if amount < 0 {
return 0, fmt.Errorf("积分计费金额无效:%d", amount)
}
return amount, nil
}