feat: 实现社区激励金(公户)系统,经济闭环
- 新增 CommunityFund / FundLog 模型(公户余额单行表 + 6种流水类型) - 新增 FundRepo 仓储(GetBalance / IncrBalance / CreateLog / ListLogs / WithTx) - 改造 EnergyService 五个方法:Energize 税收入公户、DeductOnRename/DeductOnDeletePost 入公户、RefundOnRenameReject 从公户支出、AdminAdjust 拆分为 admin_transfer/system_operation - 新增 ErrInsufficientFund 错误哨兵 - 管理后台新增公户余额展示 + 公户流水页面 + 域能调整资金来源选择 - 新增 energy.css / energy.js 前端交互 - 侧边栏新增公户流水入口
This commit is contained in:
@ -52,6 +52,7 @@ var (
|
||||
ErrInsufficientEnergy = errors.New("域能不足,可通过每日签到或创作被赋能获取")
|
||||
ErrEnergizeLimitReached = errors.New("对该文章赋能已达上限")
|
||||
ErrCreatePostNoExp = errors.New("经验不足,Lv1 解锁投稿")
|
||||
ErrInsufficientFund = errors.New("公户余额不足,无法执行操作")
|
||||
|
||||
// 评论相关
|
||||
ErrCommentNotFound = errors.New("评论不存在")
|
||||
|
||||
@ -10,10 +10,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// adminEnergyUseCase AdminEnergyController 对 EnergyService 的最小依赖(ISP:2 个方法)
|
||||
// adminEnergyUseCase AdminEnergyController 对 EnergyService 的最小依赖(ISP:4 个方法)
|
||||
type adminEnergyUseCase interface {
|
||||
AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string) error
|
||||
AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string, mode string) error
|
||||
GetAdminEnergyLogs(energyType string, page, pageSize int) ([]model.EnergyLog, int64, error)
|
||||
GetFundBalance() (int, error)
|
||||
GetFundLogs(logType string, page, pageSize int) ([]model.FundLog, int64, error)
|
||||
}
|
||||
|
||||
// AdminEnergyController 后台域能管理控制器
|
||||
@ -26,17 +28,38 @@ func NewAdminEnergyController(energySvc adminEnergyUseCase) *AdminEnergyControll
|
||||
return &AdminEnergyController{energySvc: energySvc}
|
||||
}
|
||||
|
||||
// EnergyPage 域能管理页面
|
||||
// EnergyPage 域能管理页面(公户余额 + 用户域能调整)
|
||||
func (ac *AdminEnergyController) EnergyPage(c *gin.Context) {
|
||||
balance := 0
|
||||
if b, err := ac.energySvc.GetFundBalance(); err == nil {
|
||||
balance = b
|
||||
}
|
||||
c.HTML(http.StatusOK, "admin/energy/index.html", common.BuildAdminPageData(c, gin.H{
|
||||
"Title": "域能管理",
|
||||
"Title": "域能管理",
|
||||
"ExtraCSS": "/admin/static/css/energy.css",
|
||||
"ExtraJS": "/admin/static/js/energy.js",
|
||||
"FundBalance": balance,
|
||||
"FundBalanceDisplay": float64(balance) / 10,
|
||||
}))
|
||||
}
|
||||
|
||||
// EnergyLogPage 域能日志页面
|
||||
func (ac *AdminEnergyController) EnergyLogPage(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "admin/energy/logs.html", common.BuildAdminPageData(c, gin.H{
|
||||
"Title": "域能日志",
|
||||
"Title": "域能日志",
|
||||
"ExtraCSS": "/admin/static/css/energy.css",
|
||||
"ExtraJS": "/admin/static/js/energy.js",
|
||||
"LogTypes": model.EnergyLogDisplayNames,
|
||||
}))
|
||||
}
|
||||
|
||||
// FundLogPage 公户流水页面
|
||||
func (ac *AdminEnergyController) FundLogPage(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "admin/energy/fund_logs.html", common.BuildAdminPageData(c, gin.H{
|
||||
"Title": "公户流水",
|
||||
"ExtraCSS": "/admin/static/css/energy.css",
|
||||
"ExtraJS": "/admin/static/js/energy.js",
|
||||
"LogTypes": model.FundLogDisplayNames,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -52,13 +75,32 @@ func (ac *AdminEnergyController) AdjustEnergy(c *gin.Context) {
|
||||
UserIDs []uint `json:"user_ids" binding:"required,min=1"`
|
||||
Amount int `json:"amount" binding:"required"`
|
||||
Description string `json:"description" binding:"required,min=1,max=500"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误:需要 user_ids、amount 和 description")
|
||||
return
|
||||
}
|
||||
|
||||
if err := ac.energySvc.AdminAdjust(operatorUID.(uint), req.UserIDs, req.Amount, req.Description); err != nil {
|
||||
// 默认模式为 admin_transfer
|
||||
if req.Mode == "" {
|
||||
req.Mode = model.FundTypeAdminTransfer
|
||||
}
|
||||
|
||||
// system_operation 仅 owner 可用
|
||||
if req.Mode == model.FundTypeSystemOperation {
|
||||
role, _ := c.Get("role")
|
||||
if !model.HasMinRole(role.(string), model.RoleOwner) {
|
||||
common.Error(c, http.StatusForbidden, "仅站长可使用系统操作模式")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := ac.energySvc.AdminAdjust(operatorUID.(uint), req.UserIDs, req.Amount, req.Description, req.Mode); err != nil {
|
||||
if err == common.ErrInsufficientFund {
|
||||
common.Error(c, http.StatusBadRequest, "公户余额不足,无法执行操作")
|
||||
return
|
||||
}
|
||||
common.Error(c, http.StatusInternalServerError, "调整失败")
|
||||
return
|
||||
}
|
||||
@ -101,3 +143,51 @@ func (ac *AdminEnergyController) ListEnergyLogs(c *gin.Context) {
|
||||
"total_pages": common.PageCount(total, pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
// GetFundBalance 获取公户余额
|
||||
func (ac *AdminEnergyController) GetFundBalance(c *gin.Context) {
|
||||
balance, err := ac.energySvc.GetFundBalance()
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "查询失败")
|
||||
return
|
||||
}
|
||||
common.Ok(c, gin.H{
|
||||
"balance": balance,
|
||||
"balance_display": float64(balance) / 10,
|
||||
})
|
||||
}
|
||||
|
||||
// ListFundLogs 查询公户流水
|
||||
func (ac *AdminEnergyController) ListFundLogs(c *gin.Context) {
|
||||
logType := c.Query("type")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
logs, total, err := ac.energySvc.GetFundLogs(logType, page, pageSize)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
type logView struct {
|
||||
model.FundLog
|
||||
AmountDisplay float64 `json:"amount_display"`
|
||||
TypeName string `json:"type_name"`
|
||||
}
|
||||
views := make([]logView, len(logs))
|
||||
for i, l := range logs {
|
||||
views[i] = logView{
|
||||
FundLog: l,
|
||||
AmountDisplay: float64(l.Amount) / 10,
|
||||
TypeName: model.FundLogDisplayNames[l.Type],
|
||||
}
|
||||
}
|
||||
|
||||
common.Ok(c, gin.H{
|
||||
"items": views,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total_pages": common.PageCount(total, pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
@ -86,10 +86,12 @@ type energyRenameHandler interface {
|
||||
DeductOnRename(userID uint) error
|
||||
}
|
||||
|
||||
// energyAdminUseCase AdminEnergyController 对 EnergyService 的依赖(ISP:2 个方法)
|
||||
// energyAdminUseCase AdminEnergyController 对 EnergyService 的依赖(ISP:4 个方法)
|
||||
type energyAdminUseCase interface {
|
||||
AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string) error
|
||||
AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string, mode string) error
|
||||
GetAdminEnergyLogs(energyType string, page, pageSize int) ([]model.EnergyLog, int64, error)
|
||||
GetFundBalance() (int, error)
|
||||
GetFundLogs(logType string, page, pageSize int) ([]model.FundLog, int64, error)
|
||||
}
|
||||
|
||||
// commentUseCase CommentController 对 CommentService 的最小依赖(ISP:6 个方法)
|
||||
|
||||
10
internal/model/community_fund.go
Normal file
10
internal/model/community_fund.go
Normal file
@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// CommunityFund 公户余额(单行表,id=1)
|
||||
type CommunityFund struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Balance int `gorm:"not null;default:0" json:"balance"` // 余额(×10,允许负数)
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
35
internal/model/fund_log.go
Normal file
35
internal/model/fund_log.go
Normal file
@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// 公户流水类型常量
|
||||
const (
|
||||
FundTypeEnergizeTax = "energize_tax" // 赋能税收(入公户)
|
||||
FundTypeRenameDeduction = "rename_deduction" // 改名扣费(入公户)
|
||||
FundTypeRenameRefund = "rename_refund" // 改名退款(出公户)
|
||||
FundTypeDeletePost = "delete_post_deduction" // 删稿扣费(入公户)
|
||||
FundTypeAdminTransfer = "admin_transfer" // 管理员转账(出公户,受余额约束)
|
||||
FundTypeSystemOperation = "system_operation" // 站长直调(双向,不受余额约束)
|
||||
)
|
||||
|
||||
// FundLog 公户流水记录
|
||||
type FundLog struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Amount int `gorm:"not null" json:"amount"` // +入公户 / –出公户(×10)
|
||||
Type string `gorm:"type:varchar(50);index;not null" json:"type"` // 日志类型
|
||||
RelatedType string `gorm:"type:varchar(50);default:''" json:"related_type,omitempty"`
|
||||
RelatedID uint `gorm:"default:0" json:"related_id,omitempty"`
|
||||
OperatorUID *uint `gorm:"default:null" json:"operator_uid,omitempty"`
|
||||
Description string `gorm:"type:text;not null" json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// FundLogDisplayNames 公户流水类型 → 中文名
|
||||
var FundLogDisplayNames = map[string]string{
|
||||
FundTypeEnergizeTax: "赋能税收",
|
||||
FundTypeRenameDeduction: "改名扣费",
|
||||
FundTypeRenameRefund: "改名退款",
|
||||
FundTypeDeletePost: "删稿扣费",
|
||||
FundTypeAdminTransfer: "管理转出",
|
||||
FundTypeSystemOperation: "系统操作",
|
||||
}
|
||||
60
internal/repository/fund_repo.go
Normal file
60
internal/repository/fund_repo.go
Normal file
@ -0,0 +1,60 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"metazone.cc/metalab/internal/model"
|
||||
"metazone.cc/metalab/internal/service"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// FundRepo 公户数据访问
|
||||
type FundRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewFundRepo 构造函数
|
||||
func NewFundRepo(db *gorm.DB) *FundRepo {
|
||||
return &FundRepo{db: db}
|
||||
}
|
||||
|
||||
// WithTx 基于给定事务连接创建新的 FundRepo
|
||||
func (r *FundRepo) WithTx(tx *gorm.DB) service.FundStore {
|
||||
return &FundRepo{db: tx}
|
||||
}
|
||||
|
||||
// GetBalance 查询公户余额
|
||||
func (r *FundRepo) GetBalance() (int, error) {
|
||||
var fund model.CommunityFund
|
||||
err := r.db.First(&fund, 1).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return fund.Balance, nil
|
||||
}
|
||||
|
||||
// IncrBalance 原子增减公户余额
|
||||
func (r *FundRepo) IncrBalance(amount int) error {
|
||||
return r.db.Model(&model.CommunityFund{}).
|
||||
Where("id = 1").
|
||||
UpdateColumn("balance", gorm.Expr("balance + ?", amount)).Error
|
||||
}
|
||||
|
||||
// CreateLog 创建公户流水
|
||||
func (r *FundRepo) CreateLog(log *model.FundLog) error {
|
||||
return r.db.Create(log).Error
|
||||
}
|
||||
|
||||
// ListLogs 分页查询公户流水
|
||||
func (r *FundRepo) ListLogs(logType string, offset, limit int) ([]model.FundLog, int64, error) {
|
||||
query := r.db.Model(&model.FundLog{})
|
||||
if logType != "" {
|
||||
query = query.Where("type = ?", logType)
|
||||
}
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var logs []model.FundLog
|
||||
err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error
|
||||
return logs, total, err
|
||||
}
|
||||
@ -30,6 +30,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
middleware.RequirePageRole(model.RoleOwner))
|
||||
adminPages.GET("/energy", d.adminEnergyCtrl.EnergyPage)
|
||||
adminPages.GET("/energy/logs", d.adminEnergyCtrl.EnergyLogPage)
|
||||
adminPages.GET("/energy/fund-logs", d.adminEnergyCtrl.FundLogPage)
|
||||
adminPages.GET("/comments", d.adminCommentCtrl.CommentsPage)
|
||||
}
|
||||
|
||||
@ -75,16 +76,17 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
adminPostAPI.POST("/:id/restore", d.adminPostCtrl.Restore)
|
||||
}
|
||||
|
||||
// --- 域能管理 API(admin+,调整需要 owner) ---
|
||||
// --- 域能管理 API(admin+,system_operation 仅 owner) ---
|
||||
adminEnergyAPI := r.Group("/api/admin/energy")
|
||||
adminEnergyAPI.Use(d.authMdw.Required())
|
||||
adminEnergyAPI.Use(middleware.RequireMinRole(model.RoleAdmin))
|
||||
adminEnergyAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
adminEnergyAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
adminEnergyAPI.POST("/adjust", d.adminEnergyCtrl.AdjustEnergy,
|
||||
middleware.RequireMinRole(model.RoleOwner))
|
||||
adminEnergyAPI.POST("/adjust", d.adminEnergyCtrl.AdjustEnergy)
|
||||
adminEnergyAPI.GET("/logs", d.adminEnergyCtrl.ListEnergyLogs)
|
||||
adminEnergyAPI.GET("/fund-balance", d.adminEnergyCtrl.GetFundBalance)
|
||||
adminEnergyAPI.GET("/fund-logs", d.adminEnergyCtrl.ListFundLogs)
|
||||
}
|
||||
|
||||
// --- 评论管理 API(admin+) ---
|
||||
|
||||
@ -21,7 +21,9 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
||||
|
||||
// 域能系统
|
||||
energyRepo := repository.NewEnergyRepo(db)
|
||||
fundRepo := repository.NewFundRepo(db)
|
||||
energyService := service.NewEnergyService(db, energyRepo, nil, notificationService)
|
||||
energyService.SetFundRepo(fundRepo)
|
||||
|
||||
// 等级/签到/任务系统(链式注入域能服务)
|
||||
_, levelSvc, levelCtrl := buildDepsLevel(db, notificationService, authMdw)
|
||||
|
||||
@ -19,6 +19,7 @@ type expAdder interface {
|
||||
type EnergyService struct {
|
||||
db *gorm.DB
|
||||
repo EnergyStore
|
||||
fundRepo FundStore
|
||||
expSvc expAdder
|
||||
notifSvc energyNotifier
|
||||
}
|
||||
@ -28,6 +29,16 @@ func NewEnergyService(db *gorm.DB, repo EnergyStore, expSvc expAdder, notifSvc e
|
||||
return &EnergyService{db: db, repo: repo, expSvc: expSvc, notifSvc: notifSvc}
|
||||
}
|
||||
|
||||
// SetExpAdder 链式注入经验服务(用于赋能获得经验)
|
||||
func (s *EnergyService) SetExpAdder(svc expAdder) {
|
||||
s.expSvc = svc
|
||||
}
|
||||
|
||||
// SetFundRepo 链式注入公户仓储
|
||||
func (s *EnergyService) SetFundRepo(repo FundStore) {
|
||||
s.fundRepo = repo
|
||||
}
|
||||
|
||||
// EnergyInfo 域能信息(含每日赋能经验状态)
|
||||
type EnergyInfo struct {
|
||||
Energy int // 域能余额(乘10存储)
|
||||
@ -36,11 +47,6 @@ type EnergyInfo struct {
|
||||
DailyExpCapReached bool // 是否已达上限
|
||||
}
|
||||
|
||||
// SetExpAdder 链式注入经验服务(用于赋能获得经验)
|
||||
func (s *EnergyService) SetExpAdder(svc expAdder) {
|
||||
s.expSvc = svc
|
||||
}
|
||||
|
||||
// 域能相关常量(乘10存储,DB值)
|
||||
const (
|
||||
EnergyCheckIn = 10 // +1.0 域能
|
||||
@ -69,6 +75,10 @@ func (s *EnergyService) Energize(energizerID uint, postID uint, amount int) (int
|
||||
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
txRepo := s.repo.WithTx(tx)
|
||||
var txFundRepo FundStore
|
||||
if s.fundRepo != nil {
|
||||
txFundRepo = s.fundRepo.WithTx(tx)
|
||||
}
|
||||
|
||||
// 1. 检查帖子存在,获取作者
|
||||
post, err := txRepo.FindPostByID(postID)
|
||||
@ -156,7 +166,28 @@ func (s *EnergyService) Energize(energizerID uint, postID uint, amount int) (int
|
||||
return fmt.Errorf("创建被赋能者流水失败: %w", err)
|
||||
}
|
||||
|
||||
// 10. 计算经验值(溢出截断)—— 在事务内读取以保证一致性
|
||||
// 10. 赋能税收入公户(差价 = amount - receivedAmount)
|
||||
if txFundRepo != nil {
|
||||
tax := amount - receivedAmount
|
||||
if tax > 0 {
|
||||
if err := txFundRepo.IncrBalance(tax); err != nil {
|
||||
return fmt.Errorf("公户入账失败: %w", err)
|
||||
}
|
||||
taxDisplay := float64(tax) / 10
|
||||
fundLog := &model.FundLog{
|
||||
Amount: tax,
|
||||
Type: model.FundTypeEnergizeTax,
|
||||
RelatedType: "post",
|
||||
RelatedID: postID,
|
||||
Description: fmt.Sprintf("赋能文章 #%d 税收 %.1f 域能,赋能者 uid=%d", postID, taxDisplay, energizerID),
|
||||
}
|
||||
if err := txFundRepo.CreateLog(fundLog); err != nil {
|
||||
return fmt.Errorf("创建公户流水失败: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 11. 计算经验值(溢出截断)—— 在事务内读取以保证一致性
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||
@ -188,9 +219,7 @@ func (s *EnergyService) Energize(energizerID uint, postID uint, amount int) (int
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 11. 事务提交后,在事务外增加经验 + 更新每日汇总
|
||||
// 必须在事务外执行:LevelService.AddExp 也操作 users 表,
|
||||
// 若在事务内调用会造成连接池不同连接间行锁死锁。
|
||||
// 12. 事务提交后,在事务外增加经验 + 更新每日汇总
|
||||
if needExpUpdate {
|
||||
if s.expSvc != nil {
|
||||
if _, _, err := s.expSvc.AddExp(energizerID, expGained); err != nil {
|
||||
@ -226,46 +255,91 @@ func (s *EnergyService) CheckInEnergy(userID uint) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
txRepo := s.repo.WithTx(tx)
|
||||
|
||||
if err := s.repo.AddEnergy(userID, EnergyRename); err != nil {
|
||||
return fmt.Errorf("扣减改名域能失败: %w", err)
|
||||
}
|
||||
// 检查域能余额(非 owner 需 ≥ 0)
|
||||
user, err := txRepo.FindUserByID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询用户失败: %w", err)
|
||||
}
|
||||
if user.Role != model.RoleOwner && user.Energy+EnergyRename < 0 {
|
||||
return common.ErrInsufficientEnergy
|
||||
}
|
||||
|
||||
log := &model.EnergyLog{
|
||||
UserID: userID,
|
||||
Amount: EnergyRename,
|
||||
Type: model.EnergyTypeRename,
|
||||
Description: "改名消耗 6.0 域能",
|
||||
}
|
||||
return s.repo.CreateEnergyLog(log)
|
||||
if err := txRepo.AddEnergy(userID, EnergyRename); err != nil {
|
||||
return fmt.Errorf("扣减改名域能失败: %w", err)
|
||||
}
|
||||
|
||||
// 公户入账
|
||||
if s.fundRepo != nil {
|
||||
txFundRepo := s.fundRepo.WithTx(tx)
|
||||
if err := txFundRepo.IncrBalance(-EnergyRename); err != nil { // EnergyRename = -60,入公户 +60
|
||||
return fmt.Errorf("公户入账失败: %w", err)
|
||||
}
|
||||
fundLog := &model.FundLog{
|
||||
Amount: -EnergyRename,
|
||||
Type: model.FundTypeRenameDeduction,
|
||||
RelatedType: "user",
|
||||
RelatedID: userID,
|
||||
Description: fmt.Sprintf("用户 uid=%d 改名消耗 6.0 域能", userID),
|
||||
}
|
||||
if err := txFundRepo.CreateLog(fundLog); err != nil {
|
||||
return fmt.Errorf("创建公户流水失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log := &model.EnergyLog{
|
||||
UserID: userID,
|
||||
Amount: EnergyRename,
|
||||
Type: model.EnergyTypeRename,
|
||||
Description: "改名消耗 6.0 域能",
|
||||
}
|
||||
return txRepo.CreateEnergyLog(log)
|
||||
})
|
||||
}
|
||||
|
||||
// RefundOnRenameReject 改名审核被拒,返还域能
|
||||
// RefundOnRenameReject 改名审核被拒,从公户返还域能
|
||||
func (s *EnergyService) RefundOnRenameReject(userID uint) error {
|
||||
if err := s.repo.AddEnergy(userID, EnergyRenameRefund); err != nil {
|
||||
return fmt.Errorf("返还改名域能失败: %w", err)
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
txRepo := s.repo.WithTx(tx)
|
||||
|
||||
log := &model.EnergyLog{
|
||||
UserID: userID,
|
||||
Amount: EnergyRenameRefund,
|
||||
Type: model.EnergyTypeRenameRefund,
|
||||
Description: "改名审核被拒,返还 6.0 域能",
|
||||
}
|
||||
return s.repo.CreateEnergyLog(log)
|
||||
// 从公户支出(允许透支)
|
||||
if s.fundRepo != nil {
|
||||
txFundRepo := s.fundRepo.WithTx(tx)
|
||||
if err := txFundRepo.IncrBalance(-EnergyRenameRefund); err != nil {
|
||||
return fmt.Errorf("公户出账失败: %w", err)
|
||||
}
|
||||
fundLog := &model.FundLog{
|
||||
Amount: -EnergyRenameRefund,
|
||||
Type: model.FundTypeRenameRefund,
|
||||
RelatedType: "user",
|
||||
RelatedID: userID,
|
||||
Description: fmt.Sprintf("用户 uid=%d 改名审核被拒,返还 6.0 域能", userID),
|
||||
}
|
||||
if err := txFundRepo.CreateLog(fundLog); err != nil {
|
||||
return fmt.Errorf("创建公户流水失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := txRepo.AddEnergy(userID, EnergyRenameRefund); err != nil {
|
||||
return fmt.Errorf("返还改名域能失败: %w", err)
|
||||
}
|
||||
|
||||
log := &model.EnergyLog{
|
||||
UserID: userID,
|
||||
Amount: EnergyRenameRefund,
|
||||
Type: model.EnergyTypeRenameRefund,
|
||||
Description: "改名审核被拒,返还 6.0 域能",
|
||||
}
|
||||
return txRepo.CreateEnergyLog(log)
|
||||
})
|
||||
}
|
||||
|
||||
// DeductOnDeletePost 删稿扣域能(扣文章作者的域能,无视负数)
|
||||
// 事务包裹:确保扣域能 + 写流水原子性
|
||||
// 事务包裹:扣用户域能 + 入公户 + 写流水
|
||||
func (s *EnergyService) DeductOnDeletePost(authorUserID uint, postID uint) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
txRepo := s.repo.WithTx(tx)
|
||||
@ -274,6 +348,24 @@ func (s *EnergyService) DeductOnDeletePost(authorUserID uint, postID uint) error
|
||||
return fmt.Errorf("扣减删稿域能失败: %w", err)
|
||||
}
|
||||
|
||||
// 公户入账
|
||||
if s.fundRepo != nil {
|
||||
txFundRepo := s.fundRepo.WithTx(tx)
|
||||
if err := txFundRepo.IncrBalance(-EnergyDeletePost); err != nil { // EnergyDeletePost = -20,入公户 +20
|
||||
return fmt.Errorf("公户入账失败: %w", err)
|
||||
}
|
||||
fundLog := &model.FundLog{
|
||||
Amount: -EnergyDeletePost,
|
||||
Type: model.FundTypeDeletePost,
|
||||
RelatedType: "post",
|
||||
RelatedID: postID,
|
||||
Description: fmt.Sprintf("用户 uid=%d 删除文章 #%d 消耗 2.0 域能", authorUserID, postID),
|
||||
}
|
||||
if err := txFundRepo.CreateLog(fundLog); err != nil {
|
||||
return fmt.Errorf("创建公户流水失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log := &model.EnergyLog{
|
||||
UserID: authorUserID,
|
||||
Amount: EnergyDeletePost,
|
||||
@ -287,25 +379,87 @@ func (s *EnergyService) DeductOnDeletePost(authorUserID uint, postID uint) error
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// mode: "admin_transfer"(受公户余额约束)| "system_operation"(不受约束)
|
||||
func (s *EnergyService) AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string, mode string) error {
|
||||
if mode == "" {
|
||||
mode = model.FundTypeAdminTransfer
|
||||
}
|
||||
|
||||
opUID := operatorUID
|
||||
log := &model.EnergyLog{
|
||||
UserID: uid,
|
||||
Amount: amount,
|
||||
Type: model.EnergyTypeAdminAdjust,
|
||||
Description: description,
|
||||
OperatorUID: &opUID,
|
||||
// admin_transfer 出账时检查公户余额
|
||||
if mode == model.FundTypeAdminTransfer && amount > 0 && s.fundRepo != nil {
|
||||
balance, err := s.fundRepo.GetBalance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询公户余额失败: %w", err)
|
||||
}
|
||||
if err := s.repo.CreateEnergyLog(log); err != nil {
|
||||
return fmt.Errorf("创建调整流水失败: %w", err)
|
||||
if balance < amount {
|
||||
return common.ErrInsufficientFund
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
txRepo := s.repo.WithTx(tx)
|
||||
var txFundRepo FundStore
|
||||
if s.fundRepo != nil {
|
||||
txFundRepo = s.fundRepo.WithTx(tx)
|
||||
}
|
||||
|
||||
// 公户余额调整(给用户 energy=+amount → 公户 -amount,扣用户 energy=-amount → 公户 +amount)
|
||||
if txFundRepo != nil {
|
||||
if err := txFundRepo.IncrBalance(-amount); err != nil {
|
||||
return fmt.Errorf("公户余额调整失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, uid := range userIDs {
|
||||
if err := txRepo.AddEnergy(uid, amount); err != nil {
|
||||
return fmt.Errorf("调整用户 %d 域能失败: %w", uid, err)
|
||||
}
|
||||
|
||||
opUID := operatorUID
|
||||
energyLog := &model.EnergyLog{
|
||||
UserID: uid,
|
||||
Amount: amount,
|
||||
Type: model.EnergyTypeAdminAdjust,
|
||||
Description: description,
|
||||
OperatorUID: &opUID,
|
||||
}
|
||||
if err := txRepo.CreateEnergyLog(energyLog); err != nil {
|
||||
return fmt.Errorf("创建调整流水失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 公户流水
|
||||
if txFundRepo != nil {
|
||||
amountDisplay := float64(amount) / 10
|
||||
fundLog := &model.FundLog{
|
||||
Amount: -amount,
|
||||
Type: mode,
|
||||
OperatorUID: &operatorUID,
|
||||
Description: fmt.Sprintf("%s:调整用户域能 %.1f,说明:%s", getFundModeName(mode), amountDisplay, description),
|
||||
}
|
||||
if len(userIDs) > 0 {
|
||||
fundLog.RelatedType = "user"
|
||||
fundLog.RelatedID = userIDs[0]
|
||||
}
|
||||
if err := txFundRepo.CreateLog(fundLog); err != nil {
|
||||
return fmt.Errorf("创建公户流水失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// getFundModeName 返回操作模式中文名
|
||||
func getFundModeName(mode string) string {
|
||||
switch mode {
|
||||
case model.FundTypeAdminTransfer:
|
||||
return "管理转出"
|
||||
case model.FundTypeSystemOperation:
|
||||
return "系统操作"
|
||||
default:
|
||||
return "后台调整"
|
||||
}
|
||||
}
|
||||
|
||||
// GetEnergyInfo 获取用户域能余额和每日赋能经验状态
|
||||
@ -359,3 +513,28 @@ func (s *EnergyService) GetAdminEnergyLogs(energyType string, page, pageSize int
|
||||
p.DefaultPagination()
|
||||
return s.repo.ListAllEnergyLogs(energyType, p.Offset(), p.PageSize)
|
||||
}
|
||||
|
||||
// GetFundBalance 获取公户余额
|
||||
func (s *EnergyService) GetFundBalance() (int, error) {
|
||||
if s.fundRepo == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return s.fundRepo.GetBalance()
|
||||
}
|
||||
|
||||
// GetFundLogs 分页查询公户流水
|
||||
func (s *EnergyService) GetFundLogs(logType string, page, pageSize int) ([]model.FundLog, int64, error) {
|
||||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||||
p.DefaultPagination()
|
||||
if s.fundRepo == nil {
|
||||
return []model.FundLog{}, 0, nil
|
||||
}
|
||||
logs, total, err := s.fundRepo.ListLogs(logType, p.Offset(), p.PageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if logs == nil {
|
||||
logs = []model.FundLog{}
|
||||
}
|
||||
return logs, total, nil
|
||||
}
|
||||
|
||||
@ -111,6 +111,15 @@ type EnergyStore interface {
|
||||
// energyStore 向后兼容别名
|
||||
type energyStore = EnergyStore
|
||||
|
||||
// FundStore 公户仓储接口(ISP)
|
||||
type FundStore interface {
|
||||
GetBalance() (int, error)
|
||||
IncrBalance(amount int) error
|
||||
CreateLog(log *model.FundLog) error
|
||||
ListLogs(logType string, offset, limit int) ([]model.FundLog, int64, error)
|
||||
WithTx(tx *gorm.DB) FundStore
|
||||
}
|
||||
|
||||
// energyNotifier EnergyService 所需的通知接口(ISP)
|
||||
type energyNotifier interface {
|
||||
Create(userID uint, notifyType, title, content string, relatedID, commentID *uint) error
|
||||
|
||||
Reference in New Issue
Block a user