feat: 增加友链申请、独立页面、投票/悬赏/抽奖帖与侧栏签到,并统一开发数据目录

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-27 06:23:54 +08:00
parent df19752a1e
commit 2208af7070
80 changed files with 9620 additions and 641 deletions

175
service/bounty_test.go Normal file
View File

@@ -0,0 +1,175 @@
package service
import (
"errors"
"testing"
"github.com/glebarez/sqlite"
"git.iioio.com/freefire/jiang13-forum/model"
"gorm.io/gorm"
)
func setupBountyTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.User{}, &model.Post{}, &model.Comment{}, &model.PointLedger{}); err != nil {
t.Fatal(err)
}
prev := model.DB
model.DB = db
t.Cleanup(func() { model.DB = prev })
return db
}
func seedBountyPost(t *testing.T, db *gorm.DB, authorID uint, points int) model.Post {
t.Helper()
post := model.Post{
UserID: authorID,
BoardID: 1,
Title: "悬赏测试",
Content: "内容",
PostType: model.PostTypeBounty,
BountyPoints: points,
BountyStatus: model.BountyStatusOpen,
Status: model.ContentStatusPublished,
}
if err := db.Create(&post).Error; err != nil {
t.Fatal(err)
}
return post
}
func seedUser(t *testing.T, db *gorm.DB, id uint, points int) {
t.Helper()
u := model.User{
ID: id,
Username: "user" + string(rune('0'+id)),
Password: "hash",
Nickname: "测试",
Points: points,
}
if err := db.Create(&u).Error; err != nil {
t.Fatal(err)
}
}
func seedComment(t *testing.T, db *gorm.DB, postID, userID uint, floor int, status string) {
t.Helper()
c := model.Comment{
PostID: postID,
UserID: userID,
Floor: floor,
Content: "回复",
Status: status,
}
if err := db.Create(&c).Error; err != nil {
t.Fatal(err)
}
}
func TestCountEligibleBountyReplies(t *testing.T) {
db := setupBountyTestDB(t)
post := seedBountyPost(t, db, 1, 10)
n, err := CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 0 {
t.Fatalf("无回复时期望 0得到 %d err=%v", n, err)
}
seedComment(t, db, post.ID, 1, 1, model.ContentStatusPublished)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 0 {
t.Fatalf("楼主自己的回复不应计入,得到 %d", n)
}
seedComment(t, db, post.ID, 2, 2, model.ContentStatusPublished)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 1 {
t.Fatalf("他人 published 回复期望 1得到 %d", n)
}
seedComment(t, db, post.ID, 3, 3, model.ContentStatusPending)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 1 {
t.Fatalf("pending 回复不应增加计数,得到 %d", n)
}
seedComment(t, db, post.ID, 0, 4, model.ContentStatusPublished)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 2 {
t.Fatalf("游客回复应计入,得到 %d", n)
}
}
func TestCanRefundBounty(t *testing.T) {
db := setupBountyTestDB(t)
post := seedBountyPost(t, db, 1, 5)
can, reason := CanRefundBounty(&post, false)
if !can || reason != "" {
t.Fatalf("无回复时楼主应可退can=%v reason=%q", can, reason)
}
seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished)
can, reason = CanRefundBounty(&post, false)
if can || reason != bountyRefundBlockReason {
t.Fatalf("有他人回复时楼主不可退can=%v reason=%q", can, reason)
}
can, reason = CanRefundBounty(&post, true)
if !can || reason != "" {
t.Fatalf("管理员应可强制退can=%v reason=%q", can, reason)
}
}
func TestRefundBountyBlockedForAuthorWithReplies(t *testing.T) {
db := setupBountyTestDB(t)
seedUser(t, db, 1, 0)
seedUser(t, db, 2, 0)
post := seedBountyPost(t, db, 1, 8)
seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished)
err := RefundBounty(post.ID, 1, false)
if !errors.Is(err, ErrBountyRefundBlocked) {
t.Fatalf("楼主有他人回复时应拒绝退回err=%v", err)
}
}
func TestRefundBountyAllowedWithoutReplies(t *testing.T) {
db := setupBountyTestDB(t)
seedUser(t, db, 1, 0)
post := seedBountyPost(t, db, 1, 6)
if err := RefundBounty(post.ID, 1, false); err != nil {
t.Fatalf("无回复时楼主应可退回err=%v", err)
}
var updated model.Post
if err := db.First(&updated, post.ID).Error; err != nil {
t.Fatal(err)
}
if updated.BountyStatus != model.BountyStatusRefunded || updated.BountyPoints != 0 {
t.Fatalf("状态应为 refunded 且积分为 0得到 status=%s points=%d", updated.BountyStatus, updated.BountyPoints)
}
var author model.User
if err := db.First(&author, 1).Error; err != nil {
t.Fatal(err)
}
if author.Points != 6 {
t.Fatalf("楼主应收回 6 积分,余额=%d", author.Points)
}
}
func TestRefundBountyAdminBypassWithReplies(t *testing.T) {
db := setupBountyTestDB(t)
seedUser(t, db, 1, 0)
seedUser(t, db, 2, 0)
post := seedBountyPost(t, db, 1, 4)
seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished)
if err := RefundBounty(post.ID, 99, true); err != nil {
t.Fatalf("管理员应可强制退回err=%v", err)
}
}