`)
+ for _, u := range urls {
+ u = strings.TrimSpace(u)
+ if u == "" {
+ continue
+ }
+ esc := html.EscapeString(u)
+ b.WriteString(`
`)
+ }
+ b.WriteString(`
`)
+ return b.String()
+}
+
type h5SuperArticleData struct {
Title, MetaLine, ContentHTML string
Canonical string
@@ -111,9 +131,16 @@ func H5SuperArticlePage(c *gin.Context) {
ogDesc = p
}
}
- ogImage := strings.TrimSpace(author.Avatar)
- if ogImage != "" {
- ogImage = h5AbsolutizeImage(ogImage, cfg.BaseURL)
+ articleImages := parseSuperArticleImagesJSON(row.Images)
+ ogImage := ""
+ if len(articleImages) > 0 && strings.TrimSpace(articleImages[0]) != "" {
+ ogImage = h5AbsolutizeImage(strings.TrimSpace(articleImages[0]), cfg.BaseURL)
+ }
+ if ogImage == "" {
+ ogImage = strings.TrimSpace(author.Avatar)
+ if ogImage != "" {
+ ogImage = h5AbsolutizeImage(ogImage, cfg.BaseURL)
+ }
}
shareLinkJSON, _ := json.Marshal(canonical)
signURL := h5RequestSignURL(c)
@@ -131,7 +158,7 @@ func H5SuperArticlePage(c *gin.Context) {
page := h5BuildSuperArticlePage(h5SuperArticleData{
Title: ti,
MetaLine: html.EscapeString(metaLine),
- ContentHTML: h5SuperArticleBodyHTML(row.Content),
+ ContentHTML: h5SuperArticleGalleryHTML(articleImages) + h5SuperArticleBodyHTML(row.Content),
Canonical: canonical,
OgTagsHTML: h5BuildOgTags(canonical, plainTitle, ogDesc, ogImage),
ShareLinkJSON: string(shareLinkJSON),
@@ -181,6 +208,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue"
.hdr{padding:8px 0 20px;border-bottom:1px solid rgba(255,255,255,0.08);margin-bottom:20px}
.hdr h1{font-size:22px;color:#f8fafc;line-height:1.45;font-weight:700}
.hdr .meta{font-size:13px;color:rgba(148,163,184,0.95);margin-top:10px}
+.sa-gallery{display:flex;flex-direction:column;gap:12px;margin-bottom:20px}
+.sa-img-wrap{border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,0.08)}
+.sa-gallery img{display:block;width:100%;height:auto}
.content{font-size:16px;color:#e2e8f0;line-height:1.8;word-break:break-word}
.cta{position:fixed;bottom:0;left:0;right:0;background:linear-gradient(to top,rgba(11,18,32,0.98) 45%,transparent);padding:10px 16px calc(16px + env(safe-area-inset-bottom));z-index:100}
.cta-inner{max-width:680px;margin:0 auto}
diff --git a/soul-api/internal/handler/miniprogram_super_articles.go b/soul-api/internal/handler/miniprogram_super_articles.go
index 56d5b9eb..3cdef75b 100644
--- a/soul-api/internal/handler/miniprogram_super_articles.go
+++ b/soul-api/internal/handler/miniprogram_super_articles.go
@@ -1,6 +1,7 @@
package handler
import (
+ "encoding/json"
"net/http"
"strconv"
"strings"
@@ -9,6 +10,7 @@ import (
"soul-api/internal/model"
"github.com/gin-gonic/gin"
+ "gorm.io/gorm"
)
type superArticleAuthorRow struct {
@@ -35,13 +37,163 @@ func loadSuperArticleAuthorMap(ids []string) map[string]superArticleAuthorRow {
return out
}
+const (
+ superArticleMaxImages = 9
+ superArticleMaxImageURLLen = 2048
+)
+
+func parseSuperArticleImagesJSON(raw string) []string {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return nil
+ }
+ var arr []string
+ if err := json.Unmarshal([]byte(raw), &arr); err != nil {
+ return nil
+ }
+ out := make([]string, 0, len(arr))
+ for _, u := range arr {
+ u = strings.TrimSpace(u)
+ if u != "" {
+ out = append(out, u)
+ }
+ }
+ return out
+}
+
+// superArticleFeedAllowedAuthorIDs 文章中出现过、且当前仍为超级个体的用户 ID(用于动态广场)
+func superArticleFeedAllowedAuthorIDs(db *gorm.DB) []string {
+ var raw []string
+ _ = db.Model(&model.SuperArticle{}).Distinct("user_id").Pluck("user_id", &raw).Error
+ out := make([]string, 0, len(raw))
+ seen := map[string]bool{}
+ for _, uid := range raw {
+ uid = strings.TrimSpace(uid)
+ if uid == "" || seen[uid] {
+ continue
+ }
+ seen[uid] = true
+ if _, ok := miniprogramSuperIndividualPerson(db, uid); ok {
+ out = append(out, uid)
+ }
+ }
+ return out
+}
+
+// MiniprogramSuperArticleFeed GET /api/miniprogram/super/articles/feed?page=&pageSize=
+// 动态广场:当前仍为超级个体的作者所发文章,按创建时间倒序分页。
+func MiniprogramSuperArticleFeed(c *gin.Context) {
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 30 {
+ pageSize = 10
+ }
+
+ db := database.DB()
+ allowed := superArticleFeedAllowedAuthorIDs(db)
+ if len(allowed) == 0 {
+ c.JSON(http.StatusOK, gin.H{"success": true, "list": []gin.H{}, "total": int64(0), "page": page, "pageSize": pageSize})
+ return
+ }
+
+ q := db.Model(&model.SuperArticle{}).Where("user_id IN ?", allowed)
+ var total int64
+ if err := q.Count(&total).Error; err != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
+ return
+ }
+
+ var rows []model.SuperArticle
+ if err := db.Where("user_id IN ?", allowed).
+ Order("created_at DESC").
+ Offset((page - 1) * pageSize).
+ Limit(pageSize).
+ Find(&rows).Error; err != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
+ return
+ }
+
+ idSet := map[string]bool{}
+ var authorIDs []string
+ for _, r := range rows {
+ uid := strings.TrimSpace(r.UserID)
+ if uid == "" || idSet[uid] {
+ continue
+ }
+ idSet[uid] = true
+ authorIDs = append(authorIDs, uid)
+ }
+ authorMap := loadSuperArticleAuthorMap(authorIDs)
+
+ list := make([]gin.H, 0, len(rows))
+ for _, r := range rows {
+ content := strings.TrimSpace(r.Content)
+ preview := content
+ if len([]rune(preview)) > 120 {
+ preview = string([]rune(preview)[:120]) + "..."
+ }
+ a := authorMap[strings.TrimSpace(r.UserID)]
+ list = append(list, gin.H{
+ "id": r.ID,
+ "userId": r.UserID,
+ "title": r.Title,
+ "content": r.Content,
+ "preview": preview,
+ "images": parseSuperArticleImagesJSON(r.Images),
+ "authorNickname": strings.TrimSpace(a.Nickname),
+ "authorAvatar": strings.TrimSpace(a.Avatar),
+ "createdAt": r.CreatedAt,
+ })
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "list": list,
+ "total": total,
+ "page": page,
+ "pageSize": pageSize,
+ })
+}
+
+func normalizeSuperArticleImageURLs(in []string) ([]string, string) {
+ if len(in) == 0 {
+ return []string{}, ""
+ }
+ if len(in) > superArticleMaxImages {
+ return nil, "配图最多 9 张"
+ }
+ out := make([]string, 0, len(in))
+ for _, u := range in {
+ u = strings.TrimSpace(u)
+ if u == "" {
+ continue
+ }
+ if len(u) > superArticleMaxImageURLLen {
+ return nil, "图片链接过长"
+ }
+ low := strings.ToLower(u)
+ if !strings.HasPrefix(low, "http://") && !strings.HasPrefix(low, "https://") {
+ return nil, "图片链接须以 http(s):// 开头"
+ }
+ out = append(out, u)
+ }
+ if len(out) > superArticleMaxImages {
+ return nil, "配图最多 9 张"
+ }
+ return out, ""
+}
+
// MiniprogramSuperArticleCreate POST /api/miniprogram/super/articles
// 仅超级个体本人可发文章。
func MiniprogramSuperArticleCreate(c *gin.Context) {
var req struct {
- UserID string `json:"userId"`
- Title string `json:"title"`
- Content string `json:"content"`
+ UserID string `json:"userId"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+ Images []string `json:"images"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
@@ -63,6 +215,17 @@ func MiniprogramSuperArticleCreate(c *gin.Context) {
return
}
+ imgURLs, imgErr := normalizeSuperArticleImageURLs(req.Images)
+ if imgErr != "" {
+ c.JSON(http.StatusOK, gin.H{"success": false, "error": imgErr})
+ return
+ }
+ imgJSONBytes, jerr := json.Marshal(imgURLs)
+ if jerr != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "error": "配图数据无效"})
+ return
+ }
+
db := database.DB()
if _, ok := miniprogramSuperIndividualPerson(db, req.UserID); !ok {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅超级个体可发文章", "forbidden": true})
@@ -73,6 +236,7 @@ func MiniprogramSuperArticleCreate(c *gin.Context) {
UserID: req.UserID,
Title: req.Title,
Content: req.Content,
+ Images: string(imgJSONBytes),
}
if err := db.Create(&row).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
@@ -130,6 +294,7 @@ func MiniprogramSuperArticleList(c *gin.Context) {
"title": r.Title,
"content": r.Content,
"preview": preview,
+ "images": parseSuperArticleImagesJSON(r.Images),
"authorNickname": strings.TrimSpace(author.Nickname),
"authorAvatar": strings.TrimSpace(author.Avatar),
"createdAt": r.CreatedAt,
@@ -172,6 +337,7 @@ func MiniprogramSuperArticleDetail(c *gin.Context) {
"userId": row.UserID,
"title": row.Title,
"content": row.Content,
+ "images": parseSuperArticleImagesJSON(row.Images),
"authorNickname": strings.TrimSpace(author.Nickname),
"authorAvatar": strings.TrimSpace(author.Avatar),
"createdAt": row.CreatedAt,
diff --git a/soul-api/internal/handler/upload.go b/soul-api/internal/handler/upload.go
index 2c0bedc6..abda5a53 100644
--- a/soul-api/internal/handler/upload.go
+++ b/soul-api/internal/handler/upload.go
@@ -170,7 +170,7 @@ func UploadPost(c *gin.Context) {
func uploadPostSaveLocal(c *gin.Context, file *multipart.FileHeader, folder, name, ct string) {
uploadDir := config.Get().UploadDir
if uploadDir == "" {
- uploadDir = "uploads"
+ uploadDir = "./uploads"
}
dir := filepath.Join(uploadDir, folder)
_ = os.MkdirAll(dir, 0755)
@@ -240,7 +240,7 @@ func UploadDelete(c *gin.Context) {
}
uploadDir := config.Get().UploadDir
if uploadDir == "" {
- uploadDir = "uploads"
+ uploadDir = "./uploads"
}
fullPath := filepath.Join(uploadDir, filepath.FromSlash(rel))
if err := os.Remove(fullPath); err != nil {
diff --git a/soul-api/internal/handler/upload_content.go b/soul-api/internal/handler/upload_content.go
index edca0143..5809c706 100644
--- a/soul-api/internal/handler/upload_content.go
+++ b/soul-api/internal/handler/upload_content.go
@@ -19,6 +19,7 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
+ "soul-api/internal/config"
"soul-api/internal/database"
"soul-api/internal/model"
"soul-api/internal/oss"
@@ -31,6 +32,17 @@ const (
defaultImageQuality = 85
)
+// effectiveUploadDiskDir 与 router.Static("/uploads")、upload.go uploadPostSaveLocal 一致;
+// 避免配置了 UPLOAD_DIR(绝对路径)时仍写入进程 cwd 下 ./uploads,导致 GET /uploads/… 读挂载目录 404。
+func effectiveUploadDiskDir() string {
+ if cfg := config.Get(); cfg != nil {
+ if d := strings.TrimSpace(cfg.UploadDir); d != "" {
+ return d
+ }
+ }
+ return "./uploads"
+}
+
var (
allowedImageTypes = map[string]bool{
"image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true,
@@ -140,7 +152,7 @@ func UploadImagePost(c *gin.Context) {
}
// 回退本地存储
- dir := filepath.Join(uploadDirContent, folder)
+ dir := filepath.Join(effectiveUploadDiskDir(), folder)
_ = os.MkdirAll(dir, 0755)
dst := filepath.Join(dir, name)
if err := os.WriteFile(dst, finalData, 0644); err != nil {
@@ -214,7 +226,7 @@ func UploadVideoPost(c *gin.Context) {
}
// 回退本地存储
- dir := filepath.Join(uploadDirContent, folder)
+ dir := filepath.Join(effectiveUploadDiskDir(), folder)
_ = os.MkdirAll(dir, 0755)
dst := filepath.Join(dir, name)
if err := c.SaveUploadedFile(file, dst); err != nil {
diff --git a/soul-api/internal/model/super_article.go b/soul-api/internal/model/super_article.go
index ff7512ce..7dba3482 100644
--- a/soul-api/internal/model/super_article.go
+++ b/soul-api/internal/model/super_article.go
@@ -8,6 +8,7 @@ type SuperArticle struct {
UserID string `gorm:"column:user_id;size:50;index:idx_super_articles_user_time" json:"userId"`
Title string `gorm:"column:title;size:200" json:"title"`
Content string `gorm:"column:content;type:text" json:"content"`
+ Images string `gorm:"column:images;type:text" json:"-"` // JSON 数组 ["https://..."],最多 9 条
CreatedAt time.Time `gorm:"column:created_at;index:idx_super_articles_user_time" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
diff --git a/soul-api/internal/oss/oss.go b/soul-api/internal/oss/oss.go
index 64c6bc20..80c03116 100644
--- a/soul-api/internal/oss/oss.go
+++ b/soul-api/internal/oss/oss.go
@@ -9,6 +9,7 @@ import (
"log"
"net/url"
"strings"
+ "sync"
"soul-api/internal/config"
"soul-api/internal/database"
@@ -180,23 +181,14 @@ func finalizeOssConfig(cfg *Config) bool {
return true
}
-func logOssConfigRejected(cfg *Config) {
- if cfg == nil {
- log.Printf("oss: oss_config rejected: nil cfg")
- return
- }
- switch {
- case cfg.Bucket == "":
- log.Printf("oss: oss_config rejected: missing bucket")
- case cfg.AccessKeyID == "":
- log.Printf("oss: oss_config rejected: missing accessKeyId")
- case cfg.AccessKeySecret == "" || ossSecretIsPlaceholder(cfg.AccessKeySecret):
- log.Printf("oss: oss_config rejected: missing or placeholder accessKeySecret")
- case strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(cfg.Endpoint), "https://"), "http://") == "" && inferEndpointFromRegion(cfg.Region) == "":
- log.Printf("oss: oss_config rejected: missing endpoint and region")
- default:
- log.Printf("oss: oss_config rejected: incomplete")
- }
+// 配置不完整或 Secret 为占位时走本地上传兜底;进程内只提示一次,避免并发上传刷屏。
+var logLocalFallbackOnce sync.Once
+var logOssParseOnce sync.Once
+
+func noteLocalUploadFallback() {
+ logLocalFallbackOnce.Do(func() {
+ log.Print("oss: 未启用(配置不完整或 Secret 为占位),上传已使用本地兜底;可在管理端填写 OSS 或设置环境变量 ALIYUN_OSS_ACCESS_KEY_ID / ALIYUN_OSS_ACCESS_KEY_SECRET")
+ })
}
// applyAliyunOSEnvFallback 库中 Secret 为空/占位或 AccessKeyId 为空时,用环境变量 ALIYUN_OSS_ACCESS_KEY_* 补齐(不上传 .env 到公开仓库)。
@@ -228,12 +220,14 @@ func LoadConfig() *Config {
}
cfg, err := decodeOssConfigBytes(row.ConfigValue, 0)
if err != nil {
- log.Printf("oss: oss_config parse error: %v", err)
+ logOssParseOnce.Do(func() {
+ log.Printf("oss: oss_config parse error: %v", err)
+ })
return nil
}
applyAliyunOSEnvFallback(&cfg)
if !finalizeOssConfig(&cfg) {
- logOssConfigRejected(&cfg)
+ noteLocalUploadFallback()
return nil
}
return &cfg
diff --git a/soul-api/internal/router/router.go b/soul-api/internal/router/router.go
index f813a4d0..8013876a 100644
--- a/soul-api/internal/router/router.go
+++ b/soul-api/internal/router/router.go
@@ -456,6 +456,7 @@ func Setup(cfg *config.Config) *gin.Engine {
miniprogram.GET("/my/super-stats", handler.MiniprogramMySuperStats)
miniprogram.GET("/my/super-leads", handler.MiniprogramMySuperLeads)
miniprogram.GET("/my/super-lead-tracks", handler.MiniprogramMySuperLeadTracks)
+ miniprogram.GET("/super/articles/feed", handler.MiniprogramSuperArticleFeed)
miniprogram.GET("/super/articles", handler.MiniprogramSuperArticleList)
miniprogram.GET("/super/articles/:id", handler.MiniprogramSuperArticleDetail)
miniprogram.POST("/super/articles", handler.MiniprogramSuperArticleCreate)
diff --git a/soul-api/scripts/create_super_articles.sql b/soul-api/scripts/create_super_articles.sql
index 46866e01..e40cbd42 100644
--- a/soul-api/scripts/create_super_articles.sql
+++ b/soul-api/scripts/create_super_articles.sql
@@ -1,16 +1,21 @@
-- 超级个体文章表(与 soul-api/internal/model/super_article.go、AutoMigrate 一致)
--- 线上库缺表时报:Error 1146 (42S02): Table 'soul_miniprogram.super_articles' doesn't exist
--- 在目标库执行:mysql soul_miniprogram < create_super_articles.sql
--- 或使用 Navicat 选中 soul_miniprogram 后运行本脚本。
+-- 缺表时报:Error 1146 (42S02): Table 'xxx.super_articles' doesn't exist(如 souldev、soul_miniprogram)
+-- 在目标库执行:mysql -u... -p souldev < create_super_articles.sql
+-- 或使用 Navicat 选中对应库后运行本脚本。
+-- 若 soul-api 已含 ensureSuperArticlesTableRaw:重启 API(SKIP_AUTO_MIGRATE=1 也会自动建表)即可。
CREATE TABLE IF NOT EXISTS `super_articles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` varchar(50) NOT NULL COMMENT '作者 users.id',
`title` varchar(200) NOT NULL DEFAULT '',
`content` longtext COMMENT '正文',
+ `images` longtext COMMENT '配图 URL JSON 数组,最多 9 张',
`created_at` datetime(3) DEFAULT NULL,
`updated_at` datetime(3) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_super_articles_user_time` (`user_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='超级个体发文(小程序 /s/:id H5)';
+
+-- 已有表缺列时执行:
+-- ALTER TABLE super_articles ADD COLUMN images LONGTEXT NULL COMMENT '配图 URL JSON 数组,最多 9 张';
diff --git a/配置.txy b/配置.txy
new file mode 100644
index 00000000..475d06d4
--- /dev/null
+++ b/配置.txy
@@ -0,0 +1,151 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name souldev.quwanzhi.com;
+ index index.php index.html index.htm default.php default.htm default.html;
+ root /www/wwwroot/souldev.quwanzhi.com;
+ # 以下若有自定义 location ^~ /uploads(走站点 root 静态),会与 soul-api 落盘路径不一致导致 404;应删掉 extension 里同类配置或改为反代。
+ include /www/server/panel/vhost/nginx/extension/souldev.quwanzhi.com/*.conf;
+ #CERT-APPLY-CHECK--START
+
+ # 用于SSL证书申请时的文件验证相关配置 -- 请勿删除
+ include /www/server/panel/vhost/nginx/well-known/souldev.quwanzhi.com.conf;
+ #CERT-APPLY-CHECK--END
+
+ #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
+
+ #error_page 404/404.html;
+
+ #SSL-END
+
+ #REDIRECT START
+
+ #REDIRECT END
+
+ #ERROR-PAGE-START 错误页配置,可以注释、删除或修改
+
+ #error_page 404 /404.html;
+
+ #error_page 502 /502.html;
+
+ #ERROR-PAGE-END
+
+ #PHP-INFO-START PHP引用配置,可以注释或修改
+ include enable-php-00.conf;
+ #PHP-INFO-END
+
+ #IP-RESTRICT-START 限制访问ip的配置,IP黑白名单
+
+ #IP-RESTRICT-END
+
+ #BASICAUTH START
+
+ #BASICAUTH END
+
+ #SUB_FILTER START
+
+ #SUB_FILTER END
+
+ #GZIP START
+
+ #GZIP END
+
+ #GLOBAL-CACHE START
+
+ #GLOBAL-CACHE END
+
+ # soul-api 上传目录直出(与服务器 .env 中 UPLOAD_DIR 保持一致;勿在 extension 里再写 location /uploads 以免冲突)
+ location ^~ /uploads/ {
+ alias /www/wwwroot/self/soul-api/uploads/;
+ access_log off;
+ expires 7d;
+ add_header Cache-Control "public";
+ }
+
+ #WEBSOCKET-SUPPORT START
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ #WEBSOCKET-SUPPORT END
+
+ #PROXY-CONF-START
+ location ^~ / {
+ proxy_pass http://127.0.0.1:8080;
+ proxy_set_header Host 127.0.0.1;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Real-Port $remote_port;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_set_header REMOTE-HOST $remote_addr;
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 600s;
+ proxy_read_timeout 600s;
+ # 支持websocket链接
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ }
+ #PROXY-CONF-END
+
+ #SERVER-BLOCK START
+
+ #SERVER-BLOCK END
+
+ #禁止访问的文件或目录
+ location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
+ return 404;
+ }
+ #一键申请SSL证书验证目录相关设置
+ location /.well-known {
+ allow all;
+ }
+ #禁止在证书验证目录放入敏感文件
+ if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
+ return 403;
+ }
+ #LOG START
+ access_log /www/wwwlogs/souldev.quwanzhi.com.log;
+ error_log /www/wwwlogs/souldev.quwanzhi.com.error.log;
+ #LOG END
+
+}
+# SSL server — 由脚本追加(面板有证书但未写入 443 时)
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name souldev.quwanzhi.com;
+ ssl_certificate /www/server/panel/vhost/ssl/souldev.quwanzhi.com/fullchain.pem;
+ ssl_certificate_key /www/server/panel/vhost/ssl/souldev.quwanzhi.com/privkey.pem;
+ ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
+ ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
+ ssl_prefer_server_ciphers on;
+ ssl_session_tickets on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+ add_header Strict-Transport-Security "max-age=31536000";
+ error_page 497 https://$host$request_uri;
+ location ^~ /uploads/ {
+ alias /www/wwwroot/self/soul-api/uploads/;
+ access_log off;
+ expires 7d;
+ add_header Cache-Control "public";
+ }
+ location ^~ / {
+ proxy_pass http://127.0.0.1:9092;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 600s;
+ proxy_read_timeout 600s;
+ }
+ access_log /www/wwwlogs/souldev.quwanzhi.com.log;
+ error_log /www/wwwlogs/souldev.quwanzhi.com.error.log;
+}
\ No newline at end of file