chore: sync

This commit is contained in:
2026-03-18 22:09:43 +08:00
parent 19d647c9e1
commit 091d1953e8
29 changed files with 564 additions and 1188 deletions

View File

@@ -13,7 +13,7 @@ services:
- PORT=5000
- SPROUTWORKCOLLECT_DATA_DIR=/data
# Override the default admin token (strongly recommended in production):
# - ADMIN_TOKEN=your_secure_token_here
- ADMIN_TOKEN=shumengya520
# Enable verbose Gin logging (set to 1 for debugging):
# - GIN_DEBUG=0
volumes:

View File

@@ -22,6 +22,8 @@ type Config struct {
// 2. SPROUTWORKCOLLECT_DATA_DIR / DATA_DIR (data root, works/ and config/ appended)
// 3. ./data/works and ./data/config (relative to current working directory)
func Load() *Config {
loadDotEnv()
cfg := &Config{
Port: 5000,
// Do not commit real admin tokens; override via ADMIN_TOKEN / SPROUTWORKCOLLECT_ADMIN_TOKEN.

View File

@@ -0,0 +1,60 @@
package config
import (
"os"
"strings"
)
func loadDotEnv() {
filename := ".env.local"
if isProductionEnv() {
filename = ".env.production.local"
}
loadDotEnvFile(filename)
}
func isProductionEnv() bool {
for _, key := range []string{"SPROUTWORKCOLLECT_ENV", "ENV", "GIN_MODE"} {
if v := strings.ToLower(strings.TrimSpace(os.Getenv(key))); v != "" {
return v == "production" || v == "prod" || v == "release"
}
}
return false
}
func loadDotEnvFile(filename string) {
data, err := os.ReadFile(filename)
if err != nil {
return
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.HasPrefix(trimmed, "export ") {
trimmed = strings.TrimSpace(strings.TrimPrefix(trimmed, "export "))
}
key, value, ok := strings.Cut(trimmed, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" {
continue
}
if len(value) >= 2 {
if (value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
}
if _, exists := os.LookupEnv(key); exists {
continue
}
_ = os.Setenv(key, value)
}
}

View File

@@ -2,9 +2,11 @@ package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"sproutworkcollect-backend/internal/model"
"sproutworkcollect-backend/internal/service"
)
@@ -46,16 +48,26 @@ func (h *PublicHandler) GetWorks(c *gin.Context) {
return
}
responses := make([]any, len(works))
for i, w := range works {
responses[i] = h.workSvc.BuildResponse(w)
page, pageSize, paged := resolvePageParams(c)
total := len(works)
worksPage := paginateWorks(works, page, pageSize)
ids := make([]string, len(worksPage))
for i, w := range worksPage {
ids[i] = w.WorkID
}
c.JSON(http.StatusOK, gin.H{
resp := gin.H{
"success": true,
"data": responses,
"total": len(responses),
})
"data": ids,
"total": total,
}
if paged {
resp["page"] = page
resp["page_size"] = pageSize
resp["total_pages"] = calcTotalPages(total, pageSize)
}
c.JSON(http.StatusOK, resp)
}
// GetWorkDetail handles GET /api/works/:work_id
@@ -91,16 +103,90 @@ func (h *PublicHandler) SearchWorks(c *gin.Context) {
return
}
responses := make([]any, len(works))
for i, w := range works {
responses[i] = h.workSvc.BuildResponse(w)
page, pageSize, paged := resolvePageParams(c)
total := len(works)
worksPage := paginateWorks(works, page, pageSize)
ids := make([]string, len(worksPage))
for i, w := range worksPage {
ids[i] = w.WorkID
}
c.JSON(http.StatusOK, gin.H{
resp := gin.H{
"success": true,
"data": responses,
"total": len(responses),
})
"data": ids,
"total": total,
}
if paged {
resp["page"] = page
resp["page_size"] = pageSize
resp["total_pages"] = calcTotalPages(total, pageSize)
}
c.JSON(http.StatusOK, resp)
}
func resolvePageParams(c *gin.Context) (int, int, bool) {
page, hasPage := parsePositiveInt(c, "page")
pageSize, hasSize := parsePositiveInt(c, "page_size")
if !hasSize {
pageSize, hasSize = parsePositiveInt(c, "pageSize")
}
if !hasSize {
pageSize, hasSize = parsePositiveInt(c, "limit")
}
if !hasPage && !hasSize {
return 0, 0, false
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 12
}
if pageSize > 200 {
pageSize = 200
}
return page, pageSize, true
}
func parsePositiveInt(c *gin.Context, key string) (int, bool) {
raw, ok := c.GetQuery(key)
if !ok {
return 0, false
}
val, err := strconv.Atoi(raw)
if err != nil {
return 0, false
}
return val, true
}
func paginateWorks(works []*model.WorkConfig, page, pageSize int) []*model.WorkConfig {
if pageSize <= 0 {
return works
}
if page <= 0 {
page = 1
}
start := (page - 1) * pageSize
if start >= len(works) {
return []*model.WorkConfig{}
}
end := start + pageSize
if end > len(works) {
end = len(works)
}
return works[start:end]
}
func calcTotalPages(total, pageSize int) int {
if pageSize <= 0 {
return 1
}
if total == 0 {
return 0
}
return (total + pageSize - 1) / pageSize
}
// GetCategories handles GET /api/categories

View File

@@ -7,6 +7,7 @@ type WorkConfig struct {
WorkID string `json:"作品ID"`
WorkName string `json:"作品作品"`
WorkDesc string `json:"作品描述"`
UpdateNotice string `json:"更新公告,omitempty"`
Author string `json:"作者"`
Version string `json:"作品版本号"`
Category string `json:"作品分类"`

View File

@@ -52,6 +52,14 @@ func Setup(cfg *config.Config) *gin.Engine {
admin := handler.NewAdminHandler(workSvc)
// ─── Public routes ────────────────────────────────────────────────────────
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"service": "SproutWorkCollect API",
"version": "v1",
"endpoints": []string{"/api/settings", "/api/works", "/api/works/{work_id}", "/api/search", "/api/categories", "/api/like/{work_id}"},
})
})
api := r.Group("/api")
{
api.GET("/settings", pub.GetSettings)

View File

@@ -35,6 +35,11 @@ func (s *WorkService) WorkExists(workID string) bool {
return err == nil
}
func isExternalURL(value string) bool {
trimmed := strings.TrimSpace(value)
return strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://")
}
// ─── Read operations ──────────────────────────────────────────────────────────
// LoadWork loads and returns a single work config from disk (read-locked).
@@ -191,14 +196,22 @@ func (s *WorkService) BuildResponse(w *model.WorkConfig) *model.WorkResponse {
if len(w.Screenshots) > 0 {
resp.ImageLinks = make([]string, len(w.Screenshots))
for i, img := range w.Screenshots {
resp.ImageLinks[i] = fmt.Sprintf("/api/image/%s/%s", w.WorkID, img)
if isExternalURL(img) {
resp.ImageLinks[i] = strings.TrimSpace(img)
} else {
resp.ImageLinks[i] = fmt.Sprintf("/api/image/%s/%s", w.WorkID, img)
}
}
}
if len(w.VideoFiles) > 0 {
resp.VideoLinks = make([]string, len(w.VideoFiles))
for i, vid := range w.VideoFiles {
resp.VideoLinks[i] = fmt.Sprintf("/api/video/%s/%s", w.WorkID, vid)
if isExternalURL(vid) {
resp.VideoLinks[i] = strings.TrimSpace(vid)
} else {
resp.VideoLinks[i] = fmt.Sprintf("/api/video/%s/%s", w.WorkID, vid)
}
}
}