diff --git a/miniprogram/pages/profile-edit/profile-edit.js b/miniprogram/pages/profile-edit/profile-edit.js
index 91190a14..5c9cee82 100644
--- a/miniprogram/pages/profile-edit/profile-edit.js
+++ b/miniprogram/pages/profile-edit/profile-edit.js
@@ -14,6 +14,15 @@ const { uploadByApi } = require('../../utils/miniprogramUpload.js')
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
+/** 补齐小程序可用的绝对 URL(本地/OSS 相对路径拼 baseUrl) */
+function ensureAbsMediaUrl(u, base) {
+ const s = String(u || '').trim()
+ if (!s) return ''
+ if (/^https?:\/\//i.test(s)) return s
+ const b = String(base || '').replace(/\/$/, '')
+ return b ? b + (s.startsWith('/') ? s : '/' + s) : s
+}
+
/** 首次分步完善完成后写入;与手机号+昵称齐全时自动写入,老用户免向导 */
const PROFILE_WIZARD_DONE_KEY = 'profile_wizard_v1_done'
@@ -409,6 +418,49 @@ Page({
}
},
+ /** 将接口返回的 profile data 写入 globalData.userInfo、storage;可选同步本页展示字段(头像上传时用) */
+ _syncUserCacheFromProfileData(d, patchPage) {
+ if (!d || typeof d !== 'object') return
+ const base = app.globalData.baseUrl || ''
+ const ensureAbs = (u) => ensureAbsMediaUrl(u, base)
+
+ if (patchPage) {
+ const patch = {}
+ if (d.avatar != null && String(d.avatar).trim() !== '') patch.avatar = ensureAbs(d.avatar)
+ if (d.nickname != null) patch.nickname = String(d.nickname)
+ if (d.phone != null) patch.phone = String(d.phone)
+ if (d.wechatId != null) patch.wechatId = String(d.wechatId)
+ if (d.mbti != null) {
+ patch.mbti = String(d.mbti)
+ const idx = MBTI_OPTIONS.indexOf(patch.mbti)
+ if (idx >= 0) patch.mbtiIndex = idx
+ }
+ if (Object.keys(patch).length) {
+ this.setData(patch, () => this._syncAvatarPreview())
+ } else {
+ this._syncAvatarPreview()
+ }
+ }
+
+ const u = app.globalData.userInfo
+ if (!u) return
+ if (d.nickname != null && String(d.nickname).trim() !== '') u.nickname = String(d.nickname).trim()
+ if (d.avatar != null && String(d.avatar).trim() !== '') u.avatar = ensureAbs(d.avatar)
+ if (d.phone != null && String(d.phone).trim() !== '') {
+ u.phone = String(d.phone).trim()
+ wx.setStorageSync('user_phone', u.phone)
+ }
+ if (d.wechatId != null) {
+ const w = String(d.wechatId).trim()
+ u.wechatId = w
+ u.wechat = w
+ u.wechat_id = w
+ if (w) wx.setStorageSync('user_wechat', w)
+ }
+ if (d.mbti != null && String(d.mbti).trim() !== '') u.mbti = String(d.mbti).trim()
+ wx.setStorageSync('userInfo', u)
+ },
+
/** 点击头像:相册/相机选图(不使用 open-type=chooseAvatar,避免 Windows 开发者工具 http://tmp 渲染报错) */
onTapPickAvatar() {
const run = () => {
@@ -437,20 +489,20 @@ Page({
const uploadRes = await uploadByApi({ filePath, folder: 'avatars' })
let avatarUrl = uploadRes.data?.url || uploadRes.url
- if (avatarUrl && !avatarUrl.startsWith('http')) {
- avatarUrl = app.globalData.baseUrl + avatarUrl
+ if (avatarUrl && !/^https?:\/\//i.test(avatarUrl)) {
+ avatarUrl = ensureAbsMediaUrl(avatarUrl, app.globalData.baseUrl)
}
this.setData({ avatar: avatarUrl }, () => this._syncAvatarPreview())
const avatarToSave = toAvatarPath(avatarUrl)
- await app.request({
+ const profileRes = await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: { userId: app.globalData.userInfo?.id, avatar: avatarToSave },
})
- if (app.globalData.userInfo) {
- app.globalData.userInfo.avatar = avatarUrl
- wx.setStorageSync('userInfo', app.globalData.userInfo)
+ if (!profileRes?.success) {
+ throw new Error(profileRes?.error || profileRes?.message || '保存头像失败')
}
+ this._syncUserCacheFromProfileData(profileRes.data, true)
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
setTimeout(() => this.generateShareCard(), 200)
@@ -523,12 +575,7 @@ Page({
data: payload,
})
wx.showToast({ title: '保存成功', icon: 'success' })
- if (app.globalData.userInfo) {
- if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
- if (res?.data?.avatar) app.globalData.userInfo.avatar = res.data.avatar
- if (payload.phone) app.globalData.userInfo.phone = payload.phone
- wx.setStorageSync('userInfo', app.globalData.userInfo)
- }
+ if (res?.data) this._syncUserCacheFromProfileData(res.data, false)
if (wizardComplete) {
wx.setStorageSync(PROFILE_WIZARD_DONE_KEY, '1')
this.setData({ wizardMode: false, saving: false })
diff --git a/miniprogram/pages/super-article-editor/super-article-editor.js b/miniprogram/pages/super-article-editor/super-article-editor.js
index b39def96..c41a267a 100644
--- a/miniprogram/pages/super-article-editor/super-article-editor.js
+++ b/miniprogram/pages/super-article-editor/super-article-editor.js
@@ -99,49 +99,16 @@ Page({
this.setData({ content: `${this.data.content}#链接(https://)` })
},
- chooseReferenceFiles() {
+ /** 参考素材:仅从相册/相机选图片并上传(AI 侧按图理解) */
+ chooseReferenceImages() {
this.syncAuditMode()
if (this.data.auditMode || app.globalData.auditMode) {
wx.showToast({ title: '审核模式下不可用', icon: 'none' })
return
}
- if (typeof wx.chooseMessageFile !== 'function') {
- wx.showModal({
- title: '添加参考',
- content: '当前环境不支持从聊天记录选文件,可从相册选择图片作为参考。',
- confirmText: '选图片',
- cancelText: '取消',
- success: (r) => {
- if (r.confirm) this._pickReferenceImagesFromAlbum()
- },
- })
- return
- }
- wx.chooseMessageFile({
- count: 15,
- type: 'all',
- success: (res) => {
- this.handlePickedFiles(res.tempFiles || [])
- },
- fail: (err) => {
- const em = String((err && err.errMsg) || '')
- if (/cancel|取消/i.test(em)) return
- console.warn('[super-article-editor] chooseMessageFile fail', err)
- wx.showModal({
- title: '无法打开会话文件',
- content:
- '开发者工具常不支持此能力,请用真机重试;或改用相册图片作为参考。',
- confirmText: '相册图片',
- cancelText: '取消',
- success: (r) => {
- if (r.confirm) this._pickReferenceImagesFromAlbum()
- },
- })
- },
- })
+ this._pickReferenceImagesFromAlbum()
},
- /** 相册图片兜底(路径结构与 chooseMessageFile.tempFiles 对齐) */
_pickReferenceImagesFromAlbum() {
const mapFromChooseMedia = (res) => {
const files = res.tempFiles || []
@@ -296,7 +263,7 @@ Page({
}
if (this.data.attachedFiles.some((x) => x.uploading)) {
- wx.showToast({ title: '请等待文件上传完成', icon: 'none' })
+ wx.showToast({ title: '请等待图片上传完成', icon: 'none' })
return
}
@@ -322,7 +289,7 @@ Page({
.map((x) => x.url)
if (!prompt && !referenceText && materialUrls.length === 0 && imageUrls.length === 0) {
- wx.showToast({ title: '请填写提示词或添加参考文件', icon: 'none' })
+ wx.showToast({ title: '请填写提示词或添加参考图', icon: 'none' })
return
}
diff --git a/miniprogram/pages/super-article-editor/super-article-editor.wxml b/miniprogram/pages/super-article-editor/super-article-editor.wxml
index a4384803..8745ddca 100644
--- a/miniprogram/pages/super-article-editor/super-article-editor.wxml
+++ b/miniprogram/pages/super-article-editor/super-article-editor.wxml
@@ -8,12 +8,12 @@
-
+
AI 写文章
- 参考文件(可多选)
+ 参考图片(可多选)
- + 添加文件
+ + 添加图片
@@ -25,7 +25,7 @@
移除
- 可选图片、TXT/MD/JSON 等文本;PDF/Office 将作为附件上传,服务端尽量抽取纯文本。单文件不超 30MB。会话文件请在真机微信内选取;开发者工具若无效可改用相册图片。
+ 从相册或相机添加参考图,可多选(单次最多 9 张),上传后参与 AI 生成。单张建议不超 30MB。
提示词
diff --git a/miniprogram/utils/miniprogramUpload.js b/miniprogram/utils/miniprogramUpload.js
index 4179b913..eac94d5f 100644
--- a/miniprogram/utils/miniprogramUpload.js
+++ b/miniprogram/utils/miniprogramUpload.js
@@ -3,7 +3,7 @@
*
* 必须调用 soul-api:`POST /api/miniprogram/upload`(multipart:file + folder)。
* 禁止在小程序内配置 OSS AK/SK 或直传阿里云;对象存储由服务端按
- * 「管理端 → 系统设置 → OSS」中的 oss_config 写入(未配置或失败时可落本地,以后端为准)。
+ * 「管理端 → 系统设置 → OSS」中的 oss_config 优先写 OSS;未配置或失败时服务端自动落本地 uploads/。
*
* @param {{ filePath: string, folder?: string, fileFieldName?: string }} opts
* @returns {Promise<{ success: boolean, url?: string, data?: { url?: string } }>}
diff --git a/soul-api/internal/config/config.go b/soul-api/internal/config/config.go
index e9ead40e..0cd96faf 100644
--- a/soul-api/internal/config/config.go
+++ b/soul-api/internal/config/config.go
@@ -58,7 +58,7 @@ type Config struct {
// 上传目录(绝对路径,air 运行时避免相对路径解析错误)
UploadDir string
- // UploadAllowLocalFallback 为 true 时,OSS 未配置或上传失败则落盘并用 API_BASE_URL 拼接 URL(仅建议本机调试)
+ // UploadAllowLocalFallback 历史环境变量 UPLOAD_ALLOW_LOCAL_FALLBACK:上传路由已默认在未配置/失败时落本地,该字段不再参与 handler 分支(保留读取以免外部依赖 cfg 字段缺失)。
UploadAllowLocalFallback bool
// 阿里云 OSS(可选):当库中 oss_config 的密钥为空或为占位符时兜底;与 LoadConfig 合并后再连 OSS、并在 GET /api/admin/settings 中回显生效值
diff --git a/soul-api/internal/handler/upload.go b/soul-api/internal/handler/upload.go
index 29368033..2c0bedc6 100644
--- a/soul-api/internal/handler/upload.go
+++ b/soul-api/internal/handler/upload.go
@@ -73,8 +73,7 @@ func isAllowedAttachment(file *multipart.FileHeader, ct string) bool {
// UploadPost POST /api/upload 通用上传(multipart:file + folder)
// 同源挂载:POST /api/miniprogram/upload、POST /api/admin/upload(管理端须 Admin JWT)
-// 默认仅写入阿里云 OSS 并返回 OSS 公网完整 URL(见 system_config.oss_config)。
-// 若需本机无 OSS 调试,可在环境变量设置 UPLOAD_ALLOW_LOCAL_FALLBACK=true,此时未配置或失败才落盘并用 API_BASE_URL 拼接。
+// 优先写入 OSS(system_config.oss_config);未配置或上传失败时自动落本地 uploads/ 并由 BaseURL 拼接可访问 URL(不再向前端返回 OSS 配置类错误)。
func UploadPost(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
@@ -131,16 +130,7 @@ func UploadPost(c *gin.Context) {
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrUpload(6), ext)
objectKey := filepath.ToSlash(filepath.Join("uploads", folder, name))
- allowLocal := config.Get() != nil && config.Get().UploadAllowLocalFallback
-
if !oss.IsEnabled() {
- if !allowLocal {
- c.JSON(http.StatusServiceUnavailable, gin.H{
- "success": false,
- "error": "未配置阿里云 OSS:请在管理端「系统设置 → OSS」保存完整 oss_config(AccessKey Secret 不得为占位符)。默认仅支持上传到 OSS 并返回 OSS 公网地址。",
- })
- return
- }
uploadPostSaveLocal(c, file, folder, name, ct)
return
}
@@ -169,17 +159,6 @@ func UploadPost(c *gin.Context) {
return
}
- if !allowLocal {
- msg := "上传至阿里云 OSS 失败,请检查 oss_config、Bucket 权限与网络"
- if uploadErr != nil {
- msg = "上传至 OSS 失败: " + uploadErr.Error()
- } else if url == "" {
- msg = "上传至 OSS 未返回公网 URL,请检查 endpoint、bucket 与可选 publicBaseUrl"
- }
- c.JSON(http.StatusBadGateway, gin.H{"success": false, "error": msg})
- return
- }
-
if uploadErr != nil {
log.Printf("upload: OSS failed objectKey=%s (local fallback): %v", objectKey, uploadErr)
} else {
diff --git a/soul-api/internal/handler/upload_content.go b/soul-api/internal/handler/upload_content.go
index b03b083c..edca0143 100644
--- a/soul-api/internal/handler/upload_content.go
+++ b/soul-api/internal/handler/upload_content.go
@@ -19,7 +19,6 @@ 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"
@@ -42,7 +41,7 @@ var (
)
// UploadImagePost POST /api/miniprogram/upload/image 小程序-图片上传(支持压缩)
-// 默认仅写入 OSS 并返回公网完整 URL;环境变量 UPLOAD_ALLOW_LOCAL_FALLBACK=true 时未配置或失败才落盘。
+// 优先 OSS;未配置或失败时自动落本地 uploads/。
// 表单:file(必填), folder(可选,默认 images), quality(可选 1-100,默认 85)
func UploadImagePost(c *gin.Context) {
file, err := c.FormFile("file")
@@ -116,7 +115,6 @@ func UploadImagePost(c *gin.Context) {
finalData = data
}
- allowLocal := config.Get() != nil && config.Get().UploadAllowLocalFallback
var ossURL string
var ossErr error
if oss.IsEnabled() {
@@ -135,18 +133,6 @@ func UploadImagePost(c *gin.Context) {
})
return
}
- if !allowLocal {
- msg := "未配置阿里云 OSS 或上传失败"
- if oss.IsEnabled() && ossErr != nil {
- msg = "上传至 OSS 失败: " + ossErr.Error()
- } else if !oss.IsEnabled() {
- msg = "未配置阿里云 OSS:请在管理端「系统设置 → OSS」保存完整 oss_config"
- } else {
- msg = "上传至 OSS 未返回公网地址"
- }
- c.JSON(http.StatusBadGateway, gin.H{"success": false, "error": msg})
- return
- }
if ossErr != nil {
log.Printf("upload/image: OSS failed folder=%s name=%s (local fallback): %v", folder, name, ossErr)
} else {
@@ -169,7 +155,7 @@ func UploadImagePost(c *gin.Context) {
}
// UploadVideoPost POST /api/miniprogram/upload/video 小程序-视频上传
-// 默认仅 OSS;UPLOAD_ALLOW_LOCAL_FALLBACK=true 时未配置或失败才落盘。
+// 优先 OSS;未配置或失败时自动落本地。
// 表单:file(必填), folder(可选,默认 videos)
func UploadVideoPost(c *gin.Context) {
file, err := c.FormFile("file")
@@ -196,30 +182,18 @@ func UploadVideoPost(c *gin.Context) {
}
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrContent(8), ext)
- allowLocal := config.Get() != nil && config.Get().UploadAllowLocalFallback
var ossURL string
var ossErr error
if oss.IsEnabled() {
src, openErr := file.Open()
if openErr != nil {
- if !allowLocal {
- c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "打开文件失败"})
- return
- }
ossErr = openErr
} else {
ossURL, ossErr = ossUploadFile(src, folder, name)
_ = src.Close()
}
} else {
- if !allowLocal {
- c.JSON(http.StatusServiceUnavailable, gin.H{
- "success": false,
- "error": "未配置阿里云 OSS:请在管理端「系统设置 → OSS」保存完整 oss_config",
- })
- return
- }
ossErr = fmt.Errorf("OSS 未配置")
}
@@ -235,17 +209,6 @@ func UploadVideoPost(c *gin.Context) {
return
}
- if !allowLocal {
- msg := "上传至 OSS 失败"
- if ossErr != nil {
- msg = msg + ": " + ossErr.Error()
- } else {
- msg = "上传至 OSS 未返回公网地址"
- }
- c.JSON(http.StatusBadGateway, gin.H{"success": false, "error": msg})
- return
- }
-
if ossErr != nil {
log.Printf("upload/video: OSS skipped or failed (local fallback) folder=%s name=%s: %v", folder, name, ossErr)
}