chore: sync
This commit is contained in:
@@ -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.
|
||||
|
||||
60
SproutWorkCollect-Backend-Golang/internal/config/dotenv.go
Normal file
60
SproutWorkCollect-Backend-Golang/internal/config/dotenv.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:"作品分类"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user