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:
2026-06-01 15:10:47 +08:00
parent 683c9e02d8
commit 26e557de8a
17 changed files with 1033 additions and 67 deletions

View File

@ -52,6 +52,7 @@ var (
ErrInsufficientEnergy = errors.New("域能不足,可通过每日签到或创作被赋能获取")
ErrEnergizeLimitReached = errors.New("对该文章赋能已达上限")
ErrCreatePostNoExp = errors.New("经验不足Lv1 解锁投稿")
ErrInsufficientFund = errors.New("公户余额不足,无法执行操作")
// 评论相关
ErrCommentNotFound = errors.New("评论不存在")

View File

@ -10,10 +10,12 @@ import (
"github.com/gin-gonic/gin"
)
// adminEnergyUseCase AdminEnergyController 对 EnergyService 的最小依赖ISP2 个方法)
// adminEnergyUseCase AdminEnergyController 对 EnergyService 的最小依赖ISP4 个方法)
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),
})
}

View File

@ -86,10 +86,12 @@ type energyRenameHandler interface {
DeductOnRename(userID uint) error
}
// energyAdminUseCase AdminEnergyController 对 EnergyService 的依赖ISP2 个方法)
// energyAdminUseCase AdminEnergyController 对 EnergyService 的依赖ISP4 个方法)
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 的最小依赖ISP6 个方法)

View 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"`
}

View 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: "系统操作",
}

View 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
}

View File

@ -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)
}
// --- 域能管理 APIadmin+调整需要 owner ---
// --- 域能管理 APIadmin+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)
}
// --- 评论管理 APIadmin+ ---

View File

@ -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)

View File

@ -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
}

View File

@ -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