- 修复 Admin 改名路径未扣域能(非审核路径补充 HasCompletedTask + DeductOnRename) - 赋能按钮改为 Modal 弹窗(轻赋/重赋选择 + 每日上限提示) - EnergyInfo API 增加 daily_energize_exp/daily_exp_cap_reached 返回值 - "我的域能" TAB 增加经验值和等级展示 - Energize 事务修复:导出 EnergyStore + WithTx 方法确保 DB 操作原子性 - Studio 写文章页新增 LV0 锁止横幅 + 禁用提交按钮
346 lines
10 KiB
Go
346 lines
10 KiB
Go
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"time"
|
||
|
||
"metazone.cc/metalab/internal/common"
|
||
"metazone.cc/metalab/internal/model"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// expAdder EnergyService 对经验增加的最小依赖(ISP)
|
||
type expAdder interface {
|
||
AddExp(userID uint, delta int) (newExp int, levelUp bool, err error)
|
||
}
|
||
|
||
// EnergyService 域能业务逻辑
|
||
type EnergyService struct {
|
||
db *gorm.DB
|
||
repo EnergyStore
|
||
expSvc expAdder
|
||
notifSvc energyNotifier
|
||
}
|
||
|
||
// NewEnergyService 构造函数
|
||
func NewEnergyService(db *gorm.DB, repo EnergyStore, expSvc expAdder, notifSvc energyNotifier) *EnergyService {
|
||
return &EnergyService{db: db, repo: repo, expSvc: expSvc, notifSvc: notifSvc}
|
||
}
|
||
|
||
// EnergyInfo 域能信息(含每日赋能经验状态)
|
||
type EnergyInfo struct {
|
||
Energy int // 域能余额(乘10存储)
|
||
DailyEnergizeExp int // 当日已通过赋能获得的经验值
|
||
DailyExpCap int // 每日赋能经验上限
|
||
DailyExpCapReached bool // 是否已达上限
|
||
}
|
||
|
||
// SetExpAdder 链式注入经验服务(用于赋能获得经验)
|
||
func (s *EnergyService) SetExpAdder(svc expAdder) {
|
||
s.expSvc = svc
|
||
}
|
||
|
||
// 域能相关常量(乘10存储,DB值)
|
||
const (
|
||
EnergyCheckIn = 10 // +1.0 域能
|
||
EnergyRename = -60 // -6.0 域能
|
||
EnergyRenameRefund = 60 // +6.0 域能
|
||
EnergyDeletePost = -20 // -2.0 域能
|
||
EnergyEnergize1 = 10 // 轻赋 1 域能
|
||
EnergyEnergize2 = 20 // 重赋 2 域能
|
||
EnergizeExpPerEnergy = 10 // 每 1 域能 = 10 经验
|
||
DailyEnergizeExpCap = 50 // 每日赋能经验上限
|
||
MaxEnergizePerPost = 20 // 单用户单文章最多赋能 2 域能(×10)
|
||
)
|
||
|
||
// Energize 赋能文章
|
||
// 返回:获得的经验值、错误
|
||
func (s *EnergyService) Energize(energizerID uint, postID uint, amount int) (int, error) {
|
||
// 验证赋能量
|
||
if amount != EnergyEnergize1 && amount != EnergyEnergize2 {
|
||
return 0, common.ErrInvalidEnergyAmount
|
||
}
|
||
|
||
var expGained int
|
||
|
||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||
txRepo := s.repo.WithTx(tx)
|
||
|
||
// 1. 检查帖子存在,获取作者
|
||
post, err := txRepo.FindPostByID(postID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return common.ErrPostNotFound
|
||
}
|
||
return fmt.Errorf("查询帖子失败: %w", err)
|
||
}
|
||
|
||
// 2. 不能给自己赋能
|
||
if post.UserID == energizerID {
|
||
return common.ErrCannotEnergizeSelf
|
||
}
|
||
|
||
// 3. 检查赋能者域能余额(owner 跳过)
|
||
energizer, err := txRepo.FindUserByID(energizerID)
|
||
if err != nil {
|
||
return fmt.Errorf("查询用户失败: %w", err)
|
||
}
|
||
if energizer.Role != model.RoleOwner && energizer.Energy < amount {
|
||
return common.ErrInsufficientEnergy
|
||
}
|
||
|
||
// 4. 检查单用户单文章赋能总量 ≤ MaxEnergizePerPost
|
||
total, err := txRepo.SumUserPostEnergy(energizerID, postID)
|
||
if err != nil {
|
||
return fmt.Errorf("查询赋能记录失败: %w", err)
|
||
}
|
||
if total+amount > MaxEnergizePerPost {
|
||
return common.ErrEnergizeLimitReached
|
||
}
|
||
|
||
// 5. 扣赋能者域能
|
||
if err := txRepo.AddEnergy(energizerID, -amount); err != nil {
|
||
return fmt.Errorf("扣减域能失败: %w", err)
|
||
}
|
||
|
||
// 6. 增加被赋能者(文章作者)域能(赋能量的十分之一)
|
||
receivedAmount := amount / 10 // 1 域能→0.1,2 域能→0.2
|
||
if err := txRepo.AddEnergy(post.UserID, receivedAmount); err != nil {
|
||
return fmt.Errorf("增加被赋能者域能失败: %w", err)
|
||
}
|
||
|
||
// 7. 更新 Post.total_energy_received
|
||
if err := txRepo.IncrPostEnergy(postID, amount); err != nil {
|
||
return fmt.Errorf("更新文章赋能计数失败: %w", err)
|
||
}
|
||
|
||
// 8. 插入赋能记录
|
||
eLog := &model.PostEnergizeLog{
|
||
UserID: energizerID,
|
||
PostID: postID,
|
||
Amount: amount,
|
||
}
|
||
if err := txRepo.CreateEnergizeLog(eLog); err != nil {
|
||
return fmt.Errorf("创建赋能记录失败: %w", err)
|
||
}
|
||
|
||
// 9. 写入域能流水 ×2
|
||
amountDisplay := float64(amount) / 10
|
||
receivedDisplay := float64(receivedAmount) / 10
|
||
|
||
energizerLog := &model.EnergyLog{
|
||
UserID: energizerID,
|
||
Amount: -amount,
|
||
Type: model.EnergyTypeEnergize,
|
||
RelatedType: "post",
|
||
RelatedID: postID,
|
||
Description: fmt.Sprintf("给文章 %d 赋能 %.1f 域能", postID, amountDisplay),
|
||
}
|
||
if err := txRepo.CreateEnergyLog(energizerLog); err != nil {
|
||
return fmt.Errorf("创建赋能者流水失败: %w", err)
|
||
}
|
||
|
||
energizedLog := &model.EnergyLog{
|
||
UserID: post.UserID,
|
||
Amount: receivedAmount,
|
||
Type: model.EnergyTypeEnergized,
|
||
RelatedType: "post",
|
||
RelatedID: postID,
|
||
Description: fmt.Sprintf("文章 %d 被赋能 +%.1f 域能", postID, receivedDisplay),
|
||
}
|
||
if err := txRepo.CreateEnergyLog(energizedLog); err != nil {
|
||
return fmt.Errorf("创建被赋能者流水失败: %w", err)
|
||
}
|
||
|
||
// 10. 计算经验值(溢出截断)
|
||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||
now := time.Now().In(loc)
|
||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||
|
||
summary, err := txRepo.GetDailyEnergizeExp(energizerID, today)
|
||
currentDaily := 0
|
||
if err == nil {
|
||
currentDaily = summary.ExpEarned
|
||
}
|
||
|
||
expGain := amount // 1域能(DB10) → 10经验, 2域能(DB20) → 20经验
|
||
expRoom := DailyEnergizeExpCap - currentDaily
|
||
if expRoom < 0 {
|
||
expRoom = 0
|
||
}
|
||
if expGain > expRoom {
|
||
expGain = expRoom
|
||
}
|
||
|
||
// 11. 增加经验 + 更新每日经验汇总
|
||
if expGain > 0 {
|
||
if s.expSvc != nil {
|
||
_, _, err = s.expSvc.AddExp(energizerID, expGain)
|
||
if err != nil {
|
||
return fmt.Errorf("增加经验失败: %w", err)
|
||
}
|
||
}
|
||
|
||
newSummary := &model.DailyExpSummary{
|
||
UserID: energizerID,
|
||
Date: today,
|
||
ExpEarned: currentDaily + expGain,
|
||
}
|
||
if err := txRepo.UpsertDailyExp(newSummary); err != nil {
|
||
return fmt.Errorf("更新每日经验汇总失败: %w", err)
|
||
}
|
||
}
|
||
|
||
expGained = expGain
|
||
return nil
|
||
})
|
||
|
||
return expGained, err
|
||
}
|
||
|
||
// CheckInEnergy 签到获得域能(LV0 用户不调用此方法)
|
||
func (s *EnergyService) CheckInEnergy(userID uint) error {
|
||
if err := s.repo.AddEnergy(userID, EnergyCheckIn); err != nil {
|
||
return fmt.Errorf("增加签到域能失败: %w", err)
|
||
}
|
||
|
||
log := &model.EnergyLog{
|
||
UserID: userID,
|
||
Amount: EnergyCheckIn,
|
||
Type: model.EnergyTypeSignIn,
|
||
Description: "每日签到 +1.0 域能",
|
||
}
|
||
return s.repo.CreateEnergyLog(log)
|
||
}
|
||
|
||
// DeductOnRename 改名扣域能(首次改名免费,不调用此方法)
|
||
func (s *EnergyService) DeductOnRename(userID uint) error {
|
||
// 检查域能余额(非 owner 需 ≥ 0)
|
||
user, err := s.repo.FindUserByID(userID)
|
||
if err != nil {
|
||
return fmt.Errorf("查询用户失败: %w", err)
|
||
}
|
||
if user.Role != model.RoleOwner && user.Energy+EnergyRename < 0 {
|
||
return common.ErrInsufficientEnergy
|
||
}
|
||
|
||
if err := s.repo.AddEnergy(userID, EnergyRename); err != nil {
|
||
return fmt.Errorf("扣减改名域能失败: %w", err)
|
||
}
|
||
|
||
log := &model.EnergyLog{
|
||
UserID: userID,
|
||
Amount: EnergyRename,
|
||
Type: model.EnergyTypeRename,
|
||
Description: "改名消耗 6.0 域能",
|
||
}
|
||
return s.repo.CreateEnergyLog(log)
|
||
}
|
||
|
||
// RefundOnRenameReject 改名审核被拒,返还域能
|
||
func (s *EnergyService) RefundOnRenameReject(userID uint) error {
|
||
if err := s.repo.AddEnergy(userID, EnergyRenameRefund); err != nil {
|
||
return fmt.Errorf("返还改名域能失败: %w", err)
|
||
}
|
||
|
||
log := &model.EnergyLog{
|
||
UserID: userID,
|
||
Amount: EnergyRenameRefund,
|
||
Type: model.EnergyTypeRenameRefund,
|
||
Description: "改名审核被拒,返还 6.0 域能",
|
||
}
|
||
return s.repo.CreateEnergyLog(log)
|
||
}
|
||
|
||
// DeductOnDeletePost 删稿扣域能(扣文章作者的域能,无视负数)
|
||
func (s *EnergyService) DeductOnDeletePost(authorUserID uint, postID uint) error {
|
||
if err := s.repo.AddEnergy(authorUserID, EnergyDeletePost); err != nil {
|
||
return fmt.Errorf("扣减删稿域能失败: %w", err)
|
||
}
|
||
|
||
log := &model.EnergyLog{
|
||
UserID: authorUserID,
|
||
Amount: EnergyDeletePost,
|
||
Type: model.EnergyTypeDeletePost,
|
||
RelatedType: "post",
|
||
RelatedID: postID,
|
||
Description: "删除文章消耗 2.0 域能",
|
||
}
|
||
return s.repo.CreateEnergyLog(log)
|
||
}
|
||
|
||
// AdminAdjust 后台调整用户域能
|
||
func (s *EnergyService) AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string) error {
|
||
for _, uid := range userIDs {
|
||
if err := s.repo.AddEnergy(uid, amount); err != nil {
|
||
return fmt.Errorf("调整用户 %d 域能失败: %w", uid, err)
|
||
}
|
||
|
||
opUID := operatorUID
|
||
log := &model.EnergyLog{
|
||
UserID: uid,
|
||
Amount: amount,
|
||
Type: model.EnergyTypeAdminAdjust,
|
||
Description: description,
|
||
OperatorUID: &opUID,
|
||
}
|
||
if err := s.repo.CreateEnergyLog(log); err != nil {
|
||
return fmt.Errorf("创建调整流水失败: %w", err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetEnergyInfo 获取用户域能余额和每日赋能经验状态
|
||
func (s *EnergyService) GetEnergyInfo(userID uint) (*EnergyInfo, error) {
|
||
user, err := s.repo.FindUserByID(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||
now := time.Now().In(loc)
|
||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||
|
||
dailyExp := 0
|
||
summary, err := s.repo.GetDailyEnergizeExp(userID, today)
|
||
if err == nil {
|
||
dailyExp = summary.ExpEarned
|
||
}
|
||
|
||
return &EnergyInfo{
|
||
Energy: user.Energy,
|
||
DailyEnergizeExp: dailyExp,
|
||
DailyExpCap: DailyEnergizeExpCap,
|
||
DailyExpCapReached: dailyExp >= DailyEnergizeExpCap,
|
||
}, nil
|
||
}
|
||
|
||
// GetEnergyLogs 分页查询用户域能流水(近 N 天)
|
||
func (s *EnergyService) GetEnergyLogs(userID uint, days, page, pageSize int) ([]model.EnergyLog, int64, error) {
|
||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||
p.DefaultPagination()
|
||
|
||
total, err := s.repo.CountEnergyLogsByUser(userID, days)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
logs, err := s.repo.ListEnergyLogsByUser(userID, days, p.Offset(), p.PageSize)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if logs == nil {
|
||
logs = []model.EnergyLog{}
|
||
}
|
||
return logs, total, nil
|
||
}
|
||
|
||
// GetAdminEnergyLogs 后台查询全部域能流水
|
||
func (s *EnergyService) GetAdminEnergyLogs(energyType string, page, pageSize int) ([]model.EnergyLog, int64, error) {
|
||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||
p.DefaultPagination()
|
||
return s.repo.ListAllEnergyLogs(energyType, p.Offset(), p.PageSize)
|
||
}
|