feat: enhance super article features and UI updates

- Integrated AI writing capabilities in the super article editor, allowing users to upload multiple reference files and generate drafts using prompts.
- Updated the reading page to unify the display of book chapters and super articles, improving user navigation and experience.
- Enhanced the "My" page with new UI elements, including gradient icons and improved layout for better visual appeal.
- Added backend support for new article management endpoints, ensuring seamless integration with the front end.

This update aims to streamline the article creation process and enhance the overall user interface across the application.
This commit is contained in:
乘风
2026-05-11 15:43:10 +08:00
parent e6df2b78be
commit 01f9e216da
26 changed files with 553 additions and 81 deletions

View File

@@ -52,9 +52,10 @@
| 2026-04-13 | 会议:按功能同步开发文档;页面级记 miniprogram 接口与分享行为;无接口变更亦标注 | 进行中 |
| 2026-04-14 | 吸收沉淀:逆推闭环自检写入 evolutionLOOP 需求行 | 进行中 |
| 2026-04-15 | 业务补齐:`app.js``err.response`;导师详情微信支付;找伙伴 join 失败展示后端文案 | 已完成 |
| 2026-05-09 | 我的页快捷入口:渐变面性 SVG 图标、`stat-icon-wrap` 圆形底托与卡片光效;开发文档运营与变更 / 当前小程序开发细则 §2.2.1 已同步 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-04-15
**最后更新**2026-05-09

View File

@@ -6,6 +6,10 @@
---
## 2026-05-09
- 文档同步:小程序「我的」快捷入口 UI 变更写入 `开发文档/10、项目管理/运营与变更.md``开发文档/4、前端/当前小程序开发细则.md` §2.2.1`项目索引/小程序.md` 开发进度追加
## 2026-04-15
- 吸收经验(联网):编程思维沉淀至 `agent/团队/evolution/2026-04-15.md`;升级 `role-flow-control` §1.3`经验清单`、总索引、团队/助理橙子项目索引、sync-log 已更新

View File

@@ -77,7 +77,7 @@ description: Soul 创业派对变更关联检查。miniprogram/soul-admin/soul-a
| 找伙伴/匹配 | 匹配、展示 | match、ckb 等 miniprogram 接口 | 匹配配置、开关 |
| 配置项 | 仅读取 | miniprogram/config 或 db/config | 配置编辑admin/db |
| VIP/超级个体 | 展示列表、详情 | miniprogram/vip/membersdb/users PUT、db/vip-roles | 用户列表「设置 VIP」弹窗、VIP 角色管理页 |
| 超级个体发文UGC+AI | `super-article-editor``super-article-detail` | 表 `super_articles``GET/POST` `/api/miniprogram/super/articles*``/super/articles/generate` | 当前无独立超管文章列表为常态时勿只挂 admin**勿**与 book/章节阅读接口混改或合并列表 |
| 超级个体发文UGC+AI | `super-article-editor`阅读页 **`read``superArticleId`**、`super-article-detail`(兼容重定向)、`super-article-mine` | 表 `super_articles``GET/POST/DELETE` `/api/miniprogram/super/articles*``/super/articles/generate` | 当前无独立超管文章列表为常态时勿只挂 admin**勿**与 book/章节阅读接口混改或合并列表;详情 UI 与章节共用 `read`**数据模型仍隔离** |
**专项检查**:若本次变更涉及 `super_articles`、**`super-article-*` 页面** 或 `super/articles` 接口,再读 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md),确认未与书籍/章节、MBTI 的 `soul_articles` 混表或混接口。

View File

@@ -47,7 +47,7 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
- **支付**:下单/查单走 `POST/GET /api/miniprogram/pay`、回调由 soul-api 处理;支付前必须已有 openId通过登录或 getOpenId 获得)。
- **提现**:申请/记录/确认等走 `/api/miniprogram/withdraw/*`;订阅消息模板 ID 在 `app.globalData.withdrawSubscribeTmplId`
- **scene 编解码**:海报与扫码统一用 `utils/scene.js``buildScene`/`parseScene`,支持 mid、id、ref分隔符与后端生成一致`_`)。
- **超级个体 AI 写文章**:数据落在 soul-api 表 `super_articles`,接口为 `/api/miniprogram/super/articles`(生成/发布/列表/详情),页面 `super-article-editor``super-article-detail`**勿与书籍/章节阅读流混为同一数据模型或接口**。详见 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md)。
- **超级个体 AI 写文章**:数据落在 soul-api 表 `super_articles`,接口为 `/api/miniprogram/super/articles`(生成/发布/列表/详情/删除);编辑器 `super-article-editor`**正文详情展示与书籍章节共用 `pages/read/read`,通过 query `superArticleId`(或 `saId`)进入动态模式**`super-article-detail` 仅保留对 `read` 的兼容重定向。**数据模型与书籍章节接口仍分离**,勿混表混列表。详见 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md)。
---
@@ -115,6 +115,6 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
- 做个人中心、设置页布局时(遵循 §7卡片区边距 16rpx
- 做阅读、文章等需长按复制的文本时(遵循 §9text 加 user-select
- 做编辑资料页分享名片时(遵循 §10
- 做超级个体 **AI 写文章**`super-article-editor` / `super-article-detail` 时(遵循 §4 最后一条与 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md))。
- 做超级个体 **AI 写文章**`super-article-editor`、**`read`(含 `superArticleId` 动态详情)** / `super-article-detail`(兼容跳转)时(遵循 §4 超级个体一条与 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md))。
遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。

View File

@@ -44,7 +44,7 @@ description: 超级个体 AI 写文章。super_articles 表、/api/miniprogram/s
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/miniprogram/super/articles/generate` | 根据描述 + 可选图片 URL 调用大模型,返回 `title`/`content` 草稿;需配置 OpenAI 兼容 Key/模型 |
| POST | `/api/miniprogram/super/articles/generate` | 提示词 `description` + 可选 `referenceText`(端上文本文件拼接)+ `materialUrls`(附件上传 URL服务端安全域内抓取+ `imageUrls`;四者至少一项;返回 `title`/`content` 草稿;需 OpenAI 兼容 Key/模型;契约见 `开发文档/5、接口/API接口完整文档.md` §1.3 |
| POST | `/api/miniprogram/super/articles` | 发布body`userId`, `title`, `content`;失败时可能带 `forbidden: true` |
| GET | `/api/miniprogram/super/articles?authorUserId=&page=&pageSize=` | 作者为超级个体时返回 ta 的文章列表(含 preview |
| GET | `/api/miniprogram/super/articles/:id` | 详情(作者须为超级个体,否则不可见) |
@@ -57,11 +57,14 @@ description: 超级个体 AI 写文章。super_articles 表、/api/miniprogram/s
| 页面/逻辑 | 路径 |
|-----------|------|
| AI 写文 + 发布 | [miniprogram/pages/super-article-editor/](miniprogram/pages/super-article-editor/):生成 `POST .../super/articles/generate`,发布 `POST .../super/articles`图片走 `wx.uploadFile` `/api/upload``formData.folder = article-images` |
| 详情 | [miniprogram/pages/super-article-detail/](miniprogram/pages/super-article-detail/) |
| 入口 | [miniprogram/pages/my/my.js](miniprogram/pages/my/my.js) 中 `superArticle``showSuperArticleEntry`;进编辑器前会调 `GET /api/miniprogram/my/super-stats` 校验 `isSuperIndividual` |
| AI 写文 + 发布 | [miniprogram/pages/super-article-editor/](miniprogram/pages/super-article-editor/)多文件 `chooseMessageFile`;图片上传 `folder=article-images`,附件 `folder=book-attachments`,文本可读类型可本地 read 后并入 `referenceText`生成 `POST .../super/articles/generate`timeout 建议 ≥120s,发布 `POST .../super/articles`统一 `utils/miniprogramUpload.js` `POST /api/miniprogram/upload` |
| **动态详情(正文展示)** | **`pages/read/read`**`onLoad`**`superArticleId`**(或 `saId`)时为超级个体详情模式(顶栏「文章详情」);正文/HTML、配图解析见 [miniprogram/utils/superArticleDetail.js](miniprogram/utils/superArticleDetail.js) |
| 兼容旧链 | [miniprogram/pages/super-article-detail/](miniprogram/pages/super-article-detail/):仅 **`redirectTo`** → `read?superArticleId=` |
| 我的动态列表 | [miniprogram/pages/super-article-mine/](miniprogram/pages/super-article-mine/) |
| 入口 | [miniprogram/pages/my/my.js](miniprogram/pages/my/my.js) 中 **`superArticleMine`**、`showSuperArticleEntry`;进编辑器前会调 `GET /api/miniprogram/my/super-stats` 校验 `isSuperIndividual` |
- 所有请求**必须**走 `app.request`**`/api/miniprogram/...` 前缀**(与 miniprogram-dev 一致)。
- **数据仍独立**`super_articles` 与书籍章节接口不合并;仅小程序 **UI** 共用 `read` 页面容器。
---
@@ -81,6 +84,6 @@ description: 超级个体 AI 写文章。super_articles 表、/api/miniprogram/s
## 7. 何时激活本 Skill
- 改「超级个体」「AI 写文章」「super-article-editor」super-article-detail`super_articles`
- 改「超级个体」「AI 写文章」「super-article-editor」、**阅读页动态模式(`read` + `superArticleId`**、`super-article-detail`(兼容重定向)`super_articles`
- 需区分 **本书章节**、**MBTI 引流文** 与 **超级个体发文** 时。
- 设计草稿/审核/定时发等新能力前,先读本 Skill §2 与 §5避免与现有表职责混淆。

View File

@@ -15,8 +15,8 @@ function newAttachId() {
Page({
data: {
/** 临时隐藏「AI 写文章」卡片;需上线时再设为 true */
showAiWriteSection: false,
/** 「AI 帮你写」卡片:参考图 + 提示词 + 生成 */
showAiWriteSection: true,
statusBarHeight: 44,
auditMode: false,
title: '',

View File

@@ -11,7 +11,7 @@
<!-- AI参考图片 + 提示词开关showAiWriteSection -->
<view wx:if="{{showAiWriteSection}}" class="card">
<text class="section-title">AI 帮你写</text>
<text class="label">参考图片(可选)</text>
<text class="label">参考图片(可选)</text>
<view class="file-actions">
<view class="add-file-btn" bindtap="chooseReferenceImages"> 添加图片</view>
</view>
@@ -25,7 +25,7 @@
</view>
<text class="file-remove" data-id="{{item.id}}" bindtap="removeAttachedFile">移除</text>
</view>
<text class="hint">从相册或相机添加参考图,可选(单次最多 9 张),上传后与 AI 生成。单张建议不超 30MB。</text>
<text class="hint">从相册或相机添加参考图,可选(单次最多 9 张),上传后与提示词一并交给 AI 分析。单张建议不超 30MB。</text>
<text class="label">提示词</text>
<view class="textarea-wrap">

View File

@@ -1,5 +1,5 @@
/**
* 动态广场:超级个体图文列表(触底分页)
* 动态广场:超级个体图文列表(下拉刷新 + 触底分页加载
*/
const app = getApp()
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
@@ -85,6 +85,9 @@ function mapFeedRows(rows) {
}
Page({
/** 递增序列:下拉刷新时废弃进行中的上拉请求结果,避免列表错乱 */
loadSeq: 0,
data: {
statusBarHeight: 44,
feedList: [],
@@ -126,38 +129,63 @@ Page({
this.loadMore()
},
async onPullDownRefresh() {
try {
await app.getAuditMode()
} catch (_) {}
if (app.globalData.auditMode) {
wx.stopPullDownRefresh()
return
}
try {
await this.reloadFeed()
} finally {
wx.stopPullDownRefresh()
}
},
async reloadFeed() {
this.loadSeq += 1
const seq = this.loadSeq
this.setData({
loading: true,
loadingMore: false,
page: 1,
feedList: [],
hasMore: true,
total: 0,
})
try {
await this.fetchPage(1, true)
await this.fetchPage(1, true, seq)
} finally {
this.setData({ loading: false })
if (seq === this.loadSeq) {
this.setData({ loading: false })
}
}
},
async loadMore() {
if (this.data.loading || this.data.loadingMore || !this.data.hasMore) return
const seq = this.loadSeq
const next = this.data.page + 1
this.setData({ loadingMore: true })
try {
await this.fetchPage(next, false)
await this.fetchPage(next, false, seq)
} finally {
this.setData({ loadingMore: false })
}
},
async fetchPage(page, replace) {
async fetchPage(page, replace, seq) {
const res = await app.request({
url: `/api/miniprogram/super/articles/feed?page=${encodeURIComponent(String(page))}&pageSize=${PAGE_SIZE}`,
silent: true,
})
if (seq !== this.loadSeq) {
return
}
if (!res?.success) {
if (seq !== this.loadSeq) return
wx.showToast({ title: String(res?.error || '加载失败'), icon: 'none' })
if (replace) this.setData({ feedList: [], hasMore: false, total: 0 })
return

View File

@@ -2,7 +2,7 @@
"usingComponents": {
"icon": "/components/icon/icon"
},
"enablePullDownRefresh": false,
"enablePullDownRefresh": true,
"onReachBottomDistance": 160,
"backgroundTextStyle": "light",
"backgroundColor": "#0b1220"

View File

@@ -0,0 +1,12 @@
# 超级个体「发动态」→ AI 生成OpenAI 兼容协议)
# 智增增文档https://doc.zhizengzeng.com/doc-3979947
# 接入端点基地址(勿省略 /v1
SUPER_ARTICLE_AI_BASE_URL=https://api.zhizengzeng.com/v1
# 控制台创建的 API Key勿提交到 Git复制到本机 .env / .env.development
SUPER_ARTICLE_AI_API_KEY=
# 模型名以智增增控制台/文档为准gpt-4o-mini 支持文本与参考图vision
SUPER_ARTICLE_AI_MODEL=gpt-4o-mini
# 用户上传参考图时:自动改用视觉模型多模态分析图片(与 SUPER_ARTICLE_AI_MODEL 可为不同模型)
SUPER_ARTICLE_AI_VISION_MODEL=gpt-4o-mini
# 若未配置以上项,则回退使用 OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL视觉模型缺省为 gpt-4o-mini

View File

@@ -72,6 +72,12 @@ type Config struct {
OpenAIAPIKey string // OPENAI_API_KEY
OpenAIBaseURL string // OPENAI_BASE_URL默认 https://api.openai.com/v1
OpenAIModel string // OPENAI_MODEL默认 gpt-4o-mini
// 超级个体「发动态」AI 生成(默认同上;可单独指向智增增等网关,避免与其它 OPENAI 用途混 Key
SuperArticleAIAPIKey string // SUPER_ARTICLE_AI_API_KEY空则沿用 OPENAI_API_KEY
SuperArticleAIBaseURL string // SUPER_ARTICLE_AI_BASE_URL空则沿用 OPENAI_BASE_URL
SuperArticleAIModel string // SUPER_ARTICLE_AI_MODEL空则沿用 OPENAI_MODEL纯文本生成
SuperArticleAIVisionModel string // SUPER_ARTICLE_AI_VISION_MODEL有参考图时用默认 gpt-4o-mini
}
// BaseURLJoin 将路径拼接到 BaseURLpath 应以 / 开头
@@ -326,6 +332,23 @@ func Load() (*Config, error) {
aiModel = "gpt-4o-mini"
}
superArticleAIKey := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_API_KEY"))
if superArticleAIKey == "" {
superArticleAIKey = aiAPIKey
}
superArticleAIBase := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_BASE_URL"))
if superArticleAIBase == "" {
superArticleAIBase = aiBaseURL
}
superArticleAIModel := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_MODEL"))
if superArticleAIModel == "" {
superArticleAIModel = aiModel
}
superArticleAIVisionModel := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_VISION_MODEL"))
if superArticleAIVisionModel == "" {
superArticleAIVisionModel = "gpt-4o-mini"
}
cfg := &Config{
Port: port,
Mode: mode,
@@ -362,6 +385,10 @@ func Load() (*Config, error) {
OpenAIAPIKey: aiAPIKey,
OpenAIBaseURL: aiBaseURL,
OpenAIModel: aiModel,
SuperArticleAIAPIKey: superArticleAIKey,
SuperArticleAIBaseURL: superArticleAIBase,
SuperArticleAIModel: superArticleAIModel,
SuperArticleAIVisionModel: superArticleAIVisionModel,
}
// 生产环境GIN_MODE=release强制校验敏感配置禁止使用默认值

View File

@@ -58,6 +58,7 @@ func AdminSuperArticlesList(c *gin.Context) {
list := make([]gin.H, 0, len(rows))
for _, r := range rows {
a := authorMap[strings.TrimSpace(r.UserID)]
an, aa := resolveSuperArticleAuthorDisplay(a)
list = append(list, gin.H{
"id": r.ID,
"userId": r.UserID,
@@ -66,8 +67,8 @@ func AdminSuperArticlesList(c *gin.Context) {
"images": parseSuperArticleImagesJSON(r.Images),
"auditStatus": strings.TrimSpace(r.AuditStatus),
"rejectReason": strings.TrimSpace(r.RejectReason),
"authorNickname": strings.TrimSpace(a.Nickname),
"authorAvatar": strings.TrimSpace(a.Avatar),
"authorNickname": an,
"authorAvatar": aa,
"createdAt": r.CreatedAt,
"updatedAt": r.UpdatedAt,
})

View File

@@ -98,8 +98,9 @@ func H5SuperArticlePage(c *gin.Context) {
}
authorMap := loadSuperArticleAuthorMap([]string{row.UserID})
author := authorMap[row.UserID]
nick := strings.TrimSpace(author.Nickname)
author := authorMap[strings.TrimSpace(row.UserID)]
an, aa := resolveSuperArticleAuthorDisplay(author)
nick := an
if nick == "" {
nick = "超级个体"
}
@@ -141,7 +142,7 @@ func H5SuperArticlePage(c *gin.Context) {
ogImage = h5AbsolutizeImage(strings.TrimSpace(articleImages[0]), cfg.BaseURL)
}
if ogImage == "" {
ogImage = strings.TrimSpace(author.Avatar)
ogImage = aa
if ogImage != "" {
ogImage = h5AbsolutizeImage(ogImage, cfg.BaseURL)
}

View File

@@ -22,15 +22,47 @@ import (
const aiArticleSystemPrompt = `你是「卡若创业派对」的内容创作助手,帮助超级个体写出真实、有深度的商业与创业内容文章。
写作要求:
- 标题精炼有力≤30字有吸引力点明核心价值
- 正文600-1000字有真实故事或案例有观点洞察语气自然真诚
- 风格:像朋友分享经验,而非说教;有画面感,接地气
- 不要使用"首先、其次、最后"等模板化结构词
若用户提供的素材末尾附有「写文章规则提示词」或以「---」分隔的写作规范段落你必须优先、严格按该段落成稿体裁、字数、人称、版式、Markdown 交付与自检等)。
严格输出 JSON不要有任何额外文字
输出封装(始终遵守):只输出一段合法 JSON不要有任何 JSON 外的文字。title 为主标题纯文本(可与正文首行 Markdown 标题语义一致,但不要带「#」前缀content 为正文全文,可为 Markdown含 **加粗**、分段、---、HTML 注释等),字符串内需合法 JSON 转义。
若用户未附带上述写作规范段落则默认标题精炼有力≤30字正文约6001000字语气自然真诚像朋友分享经验不要使用"首先、其次、最后"等模板化结构词。
严格输出 JSON
{"title":"文章标题","content":"文章正文"}`
// superArticleWritingRulesSuffix 在服务端拼接到用户素材末尾(与 开发文档/ai开发文档.txt 写作规则段落保持一致)。
const superArticleWritingRulesSuffix = `写文章规则提示词:
写文章。严格按以下规则成稿。
【材料】用户提供:本场聊天记录/纪要/妙记要点、场次号、主题短句;写作前先按规范阅读飞书运营报表,把报表上有的时长、场观、进房等具体数字写进开篇(只写一次)。
【人称与禁用】叙述全程用「我」,禁止「房主」。禁止「这边」「那边」。禁止「回答说,……」——问句后直接接我的陈述。语气偏强势:干脆、笃定,少「可能/也许」。「卡若」每篇最多 1 次,可完全不出现。
【刺点三对齐】正文第一行标题「# 第X场短句」、磁盘文件名、开篇刺激性金句三者同一刺点不写空泛抒情。
【结构与比例】
- 默认单场 采用「2026 每日派对干货 / 派对体」:三块约 40% / 20%30% / 余下约 30%块与块之间必须有过渡口语每一块结束必须有一段「段末小结」单独成段3050 个汉字一句,禁止出现「干货」等标签字。
【开篇】先有冲突或世俗金句,再切入场景。固定数据句式(报表有则写具体数,没有的项不写):「第 X 场X 分钟;场观 XXXXX进房 XXX 人。」不写「约」、不写「见报表」。时长/场观/进房仅在开篇出现一次。
【中段】推进用时间线或事件线多写对话「X 号问」「有人问」);我的话直接陈述。约在全文 50% 处插入一段浓缩要点 36 条2040 字/条),前后用 --- 分隔,条目中禁止「干货」二字。
【分享句】约在 20% 处一句、结尾一句,各 ≤50 字,紧扣本节,禁止「干货:」字样。
【强调·派对体成稿必选】Markdown **加粗**:至少 2 句完整立场/结论句(分布在不同段落);至少 3 个不同的重点关键词加粗。禁止堆砌无意义加粗。
【结尾】升华主题派对体可有文末小结36 条短金句)+ 总结段落 + 一句收尾金句。
【字数与真实性】小节 30005000 字;忠于来源不改原意;所有数字必须有依据、写具体算法/金额/人次等Soul 侧常用口径可参考规范中的曝光/进房/成本写法。
【隐晦植入】文中自然散布 24 处联系管理、做切片、做副业相关一句不单段喊话随机10%加率不用每篇文章都加
【版式强制】一句一行,句间空一行;分号也拆成行;段落之间空行;大段堆砌禁止。
【交付】输出 Markdown标题一行「# 第X场…」成稿末尾按模板追加 HTML 注释自检:「我」字数/次数、本场关键字频次、关键字加粗处数、重点句/词加粗是否达标。`
const (
maxArticleMaterialFetchBody = 512 * 1024 // 单素材最大读取
maxCombinedReferenceRunes = 60000 // 合并后截断上限(含提示与素材摘要)
@@ -40,11 +72,11 @@ const (
// 根据提示词 + 可选参考文本 / 素材文件 URL本站上传链接 / 图片 URL调用 AI 生成草稿(仅超级个体)。
func MiniprogramSuperArticleGenerate(c *gin.Context) {
var req struct {
UserID string `json:"userId"`
Description string `json:"description"` // 提示词(与原字段兼容)
ReferenceText string `json:"referenceText"` // 小程序端可读出的纯文本(多文件拼接)
MaterialURLs []string `json:"materialUrls"` // 上传后的文件 URL服务端在安全域名内抓取文本
ImageURLs []string `json:"imageUrls"`
UserID string `json:"userId"`
Description string `json:"description"` // 提示词(与原字段兼容)
ReferenceText string `json:"referenceText"` // 小程序端可读出的纯文本(多文件拼接)
MaterialURLs []string `json:"materialUrls"` // 上传后的文件 URL服务端在安全域名内抓取文本
ImageURLs []string `json:"imageUrls"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
@@ -86,8 +118,8 @@ func MiniprogramSuperArticleGenerate(c *gin.Context) {
}
cfg := config.Get()
if cfg == nil || strings.TrimSpace(cfg.OpenAIAPIKey) == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "AI 功能暂未开放,请联系管理员配置"})
if cfg == nil || strings.TrimSpace(cfg.SuperArticleAIAPIKey) == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "AI 功能暂未开放,请在服务端配置 SUPER_ARTICLE_AI_API_KEY 或 OPENAI_API_KEY"})
return
}
@@ -102,6 +134,7 @@ func MiniprogramSuperArticleGenerate(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": errMsg})
return
}
combined = appendSuperArticleWritingRules(combined)
title, content, err := aiGenerateArticle(cfg, combined, validImageURLs)
if err != nil {
@@ -156,6 +189,32 @@ func assembleSuperArticlePrompt(cfg *config.Config, prompt, refText string, mate
return combined, ""
}
// appendSuperArticleWritingRules 在用户提示词与参考素材组装结果末尾追加固定写作规则(保证规则段不被 maxCombinedReferenceRunes 截断)。
func appendSuperArticleWritingRules(combined string) string {
suffix := strings.TrimSpace(superArticleWritingRulesSuffix)
if suffix == "" {
return strings.TrimSpace(combined)
}
suffixRunes := len([]rune(suffix))
sep := "\n\n---\n\n"
sepLen := len([]rune(sep))
maxBody := maxCombinedReferenceRunes - suffixRunes - sepLen
if maxBody < 0 {
maxBody = 0
}
base := strings.TrimSpace(combined)
if base != "" {
rs := []rune(base)
if len(rs) > maxBody {
base = string(rs[:maxBody]) + "\n\n上文参考已截断"
}
}
if base == "" {
return suffix
}
return base + sep + suffix
}
func snippetNameFromURL(raw string) string {
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil {
@@ -281,34 +340,66 @@ func looksLikeUTF8ArticleRef(b []byte) bool {
return float64(printable)/float64(total) > 0.90
}
// superArticleModelLikelySupportsVision 粗略判断 chat/completions 模型是否适合传 image_url智增增/OpenAI 等多网关)。
func superArticleModelLikelySupportsVision(model string) bool {
m := strings.ToLower(strings.TrimSpace(model))
if m == "" {
return false
}
if strings.Contains(m, "vision") || strings.Contains(m, "-vl") || strings.Contains(m, "vqa") {
return true
}
if strings.Contains(m, "gpt-4") || strings.Contains(m, "gpt-5") || strings.Contains(m, "gpt-4o") {
return true
}
if strings.Contains(m, "claude-3") || strings.Contains(m, "claude-4") {
return true
}
if strings.Contains(m, "glm-4v") || strings.Contains(m, "qwen-vl") || strings.Contains(m, "gemini") {
return true
}
return false
}
// aiGenerateArticle 调用 OpenAI 兼容 API 生成文章标题和正文。
// 若有图片 URL模型支持视觉,以多模态格式传入;否则退化为纯文本
// 若有参考图片 URL:使用多模态 user contenttext + image_url当前主模型支持 vision 时自动改用 SUPER_ARTICLE_AI_VISION_MODEL默认同为 gpt-4o-mini
func aiGenerateArticle(cfg *config.Config, description string, imageURLs []string) (title, content string, err error) {
apiKey := strings.TrimSpace(cfg.OpenAIAPIKey)
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.OpenAIBaseURL), "/")
apiKey := strings.TrimSpace(cfg.SuperArticleAIAPIKey)
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.SuperArticleAIBaseURL), "/")
if baseURL == "" {
baseURL = "https://api.openai.com/v1"
}
model := strings.TrimSpace(cfg.OpenAIModel)
if model == "" {
model = "gpt-4o-mini"
baseModel := strings.TrimSpace(cfg.SuperArticleAIModel)
if baseModel == "" {
baseModel = "gpt-4o-mini"
}
// 判断模型是否支持视觉OpenAI gpt-4 系列 / claude-3 系列)
supportsVision := len(imageURLs) > 0 && (
strings.Contains(model, "gpt-4") ||
strings.Contains(model, "claude-3") ||
strings.Contains(model, "vision"))
requestModel := baseModel
useVision := len(imageURLs) > 0
if useVision {
if superArticleModelLikelySupportsVision(baseModel) {
requestModel = baseModel
} else {
vm := strings.TrimSpace(cfg.SuperArticleAIVisionModel)
if vm == "" {
vm = "gpt-4o-mini"
}
requestModel = vm
}
}
var userContent interface{}
if supportsVision {
if useVision {
parts := []map[string]interface{}{
{"type": "text", "text": buildUserPrompt(description, imageURLs, true)},
}
for _, u := range imageURLs {
parts = append(parts, map[string]interface{}{
"type": "image_url",
"image_url": map[string]string{"url": u, "detail": "low"},
"type": "image_url",
"image_url": map[string]string{
"url": u,
"detail": "low",
},
})
}
userContent = parts
@@ -317,16 +408,16 @@ func aiGenerateArticle(cfg *config.Config, description string, imageURLs []strin
}
reqBody, _ := json.Marshal(map[string]interface{}{
"model": model,
"model": requestModel,
"messages": []map[string]interface{}{
{"role": "system", "content": aiArticleSystemPrompt},
{"role": "user", "content": userContent},
},
"temperature": 0.8,
"max_tokens": 2000,
"max_tokens": 8192,
})
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
httpReq, err := http.NewRequestWithContext(ctx, "POST", baseURL+"/chat/completions", bytes.NewReader(reqBody))
@@ -341,7 +432,10 @@ func aiGenerateArticle(cfg *config.Config, description string, imageURLs []strin
return "", "", fmt.Errorf("请求 AI 服务失败: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, errRead := io.ReadAll(resp.Body)
if errRead != nil {
return "", "", fmt.Errorf("读取 AI 响应失败: %v", errRead)
}
var apiResp struct {
Choices []struct {
@@ -354,10 +448,23 @@ func aiGenerateArticle(cfg *config.Config, description string, imageURLs []strin
} `json:"error"`
}
if err := json.Unmarshal(body, &apiResp); err != nil {
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", "", fmt.Errorf("AI 服务 HTTP %d", resp.StatusCode)
}
return "", "", fmt.Errorf("解析 AI 响应失败")
}
if apiResp.Error != nil {
return "", "", fmt.Errorf("%s", apiResp.Error.Message)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
if apiResp.Error != nil && strings.TrimSpace(apiResp.Error.Message) != "" {
return "", "", fmt.Errorf("%s", strings.TrimSpace(apiResp.Error.Message))
}
snippet := strings.TrimSpace(string(body))
if len(snippet) > 400 {
snippet = snippet[:400] + "…"
}
return "", "", fmt.Errorf("AI 服务 HTTP %d %s", resp.StatusCode, snippet)
}
if apiResp.Error != nil && strings.TrimSpace(apiResp.Error.Message) != "" {
return "", "", fmt.Errorf("%s", strings.TrimSpace(apiResp.Error.Message))
}
if len(apiResp.Choices) == 0 || apiResp.Choices[0].Message.Content == "" {
return "", "", fmt.Errorf("AI 无返回内容")
@@ -410,12 +517,12 @@ func parseArticleJSON(raw string) (title, content string, err error) {
return "", "", fmt.Errorf("AI 生成内容不完整,请重试")
}
// 截断超限内容
if tr := []rune(title); len(tr) > 40 {
title = string(tr[:40])
// 截断超限内容(派对体长文可到数千字,预留上限避免极端响应撑爆存储)
if tr := []rune(title); len(tr) > 120 {
title = string(tr[:120])
}
if cr := []rune(content); len(cr) > 5000 {
content = string(cr[:5000])
if cr := []rune(content); len(cr) > 20000 {
content = string(cr[:20000])
}
return title, content, nil

View File

@@ -14,9 +14,24 @@ import (
)
type superArticleAuthorRow struct {
ID string `gorm:"column:id"`
Nickname string `gorm:"column:nickname"`
Avatar string `gorm:"column:avatar"`
ID string `gorm:"column:id"`
Nickname string `gorm:"column:nickname"`
Avatar string `gorm:"column:avatar"`
VipName string `gorm:"column:vip_name"`
VipAvatar string `gorm:"column:vip_avatar"`
}
// resolveSuperArticleAuthorDisplay 展示用昵称/头像:普通资料优先,超级个体常用 vip_* 兜底。
func resolveSuperArticleAuthorDisplay(a superArticleAuthorRow) (nickname, avatar string) {
nickname = strings.TrimSpace(a.Nickname)
if nickname == "" {
nickname = strings.TrimSpace(a.VipName)
}
avatar = strings.TrimSpace(a.Avatar)
if avatar == "" {
avatar = strings.TrimSpace(a.VipAvatar)
}
return nickname, avatar
}
func loadSuperArticleAuthorMap(ids []string) map[string]superArticleAuthorRow {
@@ -26,7 +41,7 @@ func loadSuperArticleAuthorMap(ids []string) map[string]superArticleAuthorRow {
}
var rows []superArticleAuthorRow
db := database.DB()
_ = db.Table("users").Select("id", "nickname", "avatar").Where("id IN ?", ids).Find(&rows).Error
_ = db.Table("users").Select("id", "nickname", "avatar", "vip_name", "vip_avatar").Where("id IN ?", ids).Find(&rows).Error
for _, r := range rows {
key := strings.TrimSpace(r.ID)
if key == "" {
@@ -142,6 +157,7 @@ func MiniprogramSuperArticleFeed(c *gin.Context) {
preview = string([]rune(preview)[:120]) + "..."
}
a := authorMap[strings.TrimSpace(r.UserID)]
an, aa := resolveSuperArticleAuthorDisplay(a)
list = append(list, gin.H{
"id": r.ID,
"userId": r.UserID,
@@ -149,8 +165,8 @@ func MiniprogramSuperArticleFeed(c *gin.Context) {
"content": r.Content,
"preview": preview,
"images": parseSuperArticleImagesJSON(r.Images),
"authorNickname": strings.TrimSpace(a.Nickname),
"authorAvatar": strings.TrimSpace(a.Avatar),
"authorNickname": an,
"authorAvatar": aa,
"createdAt": r.CreatedAt,
})
}
@@ -343,6 +359,7 @@ func MiniprogramSuperArticleList(c *gin.Context) {
authorMap := loadSuperArticleAuthorMap([]string{authorUserID})
author := authorMap[authorUserID]
an, aa := resolveSuperArticleAuthorDisplay(author)
list := make([]gin.H, 0, len(rows))
for _, r := range rows {
content := strings.TrimSpace(r.Content)
@@ -357,8 +374,8 @@ func MiniprogramSuperArticleList(c *gin.Context) {
"content": r.Content,
"preview": preview,
"images": parseSuperArticleImagesJSON(r.Images),
"authorNickname": strings.TrimSpace(author.Nickname),
"authorAvatar": strings.TrimSpace(author.Avatar),
"authorNickname": an,
"authorAvatar": aa,
"createdAt": r.CreatedAt,
})
}
@@ -409,6 +426,7 @@ func MiniprogramSuperArticleMine(c *gin.Context) {
authorMap := loadSuperArticleAuthorMap([]string{userID})
author := authorMap[userID]
an, aa := resolveSuperArticleAuthorDisplay(author)
list := make([]gin.H, 0, len(rows))
for _, r := range rows {
content := strings.TrimSpace(r.Content)
@@ -427,8 +445,8 @@ func MiniprogramSuperArticleMine(c *gin.Context) {
"content": r.Content,
"preview": preview,
"images": parseSuperArticleImagesJSON(r.Images),
"authorNickname": strings.TrimSpace(author.Nickname),
"authorAvatar": strings.TrimSpace(author.Avatar),
"authorNickname": an,
"authorAvatar": aa,
"createdAt": r.CreatedAt,
"auditStatus": ast,
"rejectReason": strings.TrimSpace(r.RejectReason),
@@ -474,7 +492,8 @@ func MiniprogramSuperArticleDetail(c *gin.Context) {
ast = model.SuperArticleAuditApproved
}
authorMap := loadSuperArticleAuthorMap([]string{row.UserID})
author := authorMap[row.UserID]
author := authorMap[strings.TrimSpace(row.UserID)]
an, aa := resolveSuperArticleAuthorDisplay(author)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
@@ -483,8 +502,8 @@ func MiniprogramSuperArticleDetail(c *gin.Context) {
"title": row.Title,
"content": row.Content,
"images": parseSuperArticleImagesJSON(row.Images),
"authorNickname": strings.TrimSpace(author.Nickname),
"authorAvatar": strings.TrimSpace(author.Avatar),
"authorNickname": an,
"authorAvatar": aa,
"createdAt": row.CreatedAt,
"auditStatus": ast,
"rejectReason": strings.TrimSpace(row.RejectReason),

View File

@@ -6,6 +6,27 @@
## 一、近期同步(摘要)
### 2026-05-09 超级个体「动态详情」并入小程序阅读页(文章详情)
- **动机**:产品与章节阅读统一为同一套「文章详情」承载页;动态分享与 H5 拉起小程序路径对齐。
- **小程序**`pages/read/read?superArticleId={数字}`(兼容 `saId``data.pageMode``super_article` 时渲染动态 UI顶栏文案「文章详情」。解析逻辑见 `miniprogram/utils/superArticleDetail.js`
- **兼容**`pages/super-article-detail` 保留注册,`onLoad``redirectTo``read?superArticleId=`
- **入口更新**`super-article-mine``super-moments``member-detail` 等跳转已改为阅读页 query。
- **后端**`h5_super_article.go` 中拉起小程序 path 已改为 `pages/read/read?superArticleId=...`
- **文档**`开发文档/4、前端/当前小程序开发细则.md` §2.2.2。
### 2026-05-09 小程序「我的」页快捷入口 UI 优化
- **范围**`pages/my/my.wxml``pages/my/my.wxss`,以及 `miniprogram/assets/icons/list-teal.svg``share-teal.svg``wallet-teal.svg``users-teal.svg``book-arrow-teal.svg`
- **说明**:快捷入口图标改为渐变面性 SVG每项增加圆形发光底托`stat-icon-wrap`)、卡片渐变边框与弱外发光;余额主文案使用 `stat-num-balance`(渐变字,低版本可降级纯色)。**`book-arrow-teal.svg`** 与「最近阅读」卡片标题共用,改图标时需注意两处观感一致。
- **交叉引用**:实现约定见 `开发文档/4、前端/当前小程序开发细则.md` §2.2.1。
### 2026-05-09 超级个体「AI 写文章」:多文件 + 提示词 + 生成草稿
- **小程序**`pages/super-article-editor/super-article-editor``wx.chooseMessageFile` 多选;图片上传 `folder=article-images`,附件 `folder=book-attachments`,文本类可本地读入后随 `referenceText` 提交;`POST /api/miniprogram/super/articles/generate` 建议超时 120s上传走 `utils/miniprogramUpload.js`Bearer
- **后端**`/super/articles/generate` 支持 `description``referenceText``materialUrls``imageUrls`,至少其一;`materialUrls` 仅允许白名单域拉取(防 SSRF
- **文档**`开发文档/5、接口/API接口完整文档.md` §1.3、`开发文档/三端需求业务对齐-小程序与API.md` §四、`开发文档/4、前端/当前小程序开发细则.md` §2.2.3、`.cursor/skills/super-individual-article/SKILL.md`
### 2026-04-28 超级个体发文章接口对接与排障
- **目标**:小程序超级个体发布文章,统一走 `super_articles` 表对应接口,避免与管理端文章体系混用。
@@ -34,4 +55,4 @@
---
**最后更新**2026-04-15
**最后更新**2026-05-09

View File

@@ -1,7 +1,7 @@
# 当前小程序开发细则
> 汇总当前 Soul 创业派对小程序的架构、经验与规划,便于新人上手与后续迭代。
> 最后整理2026-02
> 最后整理2026-05-09§2.2.2 阅读页承载超级个体动态§2.2.3 AI 发文章)
---
@@ -52,6 +52,33 @@ miniprogram/
非 Tab 页:阅读页 read、推广中心 referral、订单 purchases、设置 settings、搜索 search、关于 about。
### 2.2.1 我的页「快捷入口」UI 约定2026-05
- **位置**`pages/my/my` 中「快捷入口」卡片:`stats-card``stats-grid` → 若干 `stat-box`
- **结构**:每个入口为 `stat-box`;图标外包一层 `stat-icon-wrap`(圆形渐变底、描边与弱外发光);列表项设 `hover-class="stat-box-hover"` 提供按压反馈。
- **图标资源**`miniprogram/assets/icons/``list-teal`(订单)、`share-teal`(代付)、`wallet-teal`(余额)、`users-teal`(客资,条件展示)、`book-arrow-teal`(发文,条件展示)。均为青绿渐变面性 SVG。
- **共用资源**`book-arrow-teal.svg` 同时用于同页「最近阅读」卡片标题左侧图标,修改该文件需兼顾两处视觉。
- **余额文案**:主数字 class 为 `stat-num-balance`(渐变字 + 等宽数字倾向);若某端 `background-clip: text` 表现不佳,可降级为纯色 `#4FD1C5`
### 2.2.2 阅读页 `read` 双模式:书籍章节 + 超级个体动态(文章详情)
- **同一页面**`pages/read/read` 既承载**书籍章节阅读**(付费墙、上下篇、阅读进度等),也承载 **超级个体 UGC 动态的正文详情展示**(与原先独立「动态详情」页合并)。
- **区分参数(必选其一语义)**
- **章节**`id`(章节 id、可选 `mid`(章节 mid、及原有 `ref`/`gift`/scene 等;逻辑不变。
- **动态****`superArticleId`**(数字字符串,表 `super_articles.id`);兼容简写 **`saId`**。**不要**用章节用的 `id` 传动态 id避免与章节 id 混淆。
- **落地路径示例**`/pages/read/read?superArticleId=123`;分享卡片 / 朋友圈已为该格式。
- **兼容**`pages/super-article-detail/super-article-detail?id=` 仍注册在 `app.json`,进入后 **`redirectTo`** 到上述 `read?superArticleId=`(旧分享链接可用)。
- **相关页面**`super-article-editor`(编辑/发布)、`super-article-mine`(我的动态列表)、`super-moments`(广场)、`member-detail`(嘉宾动态入口)等跳转详情时统一 **`navigateTo` 阅读页 + `superArticleId`**。
- **工具**:动态详情用到的正文/HTML、配图解析见 `miniprogram/utils/superArticleDetail.js`(由 `read.js` 引用)。
- **后端 H5**:拉起小程序路径已对齐为 `pages/read/read?superArticleId=...`(见 `soul-api/internal/handler/h5_super_article.go`)。
### 2.2.3 超级个体「发文章」页:多文件参考 + AI 生成 + 发布2026-05
- **页面**`pages/super-article-editor/super-article-editor`
- **流程**:选择多份参考文件 → 填写提示词 → **生成文章**`POST /api/miniprogram/super/articles/generate`,建议 `timeout` ≥ 120s自动回填标题与正文 → 可微调后 **发布文章**`POST /api/miniprogram/super/articles`)。
- **选文件**`wx.chooseMessageFile` 多选。**图片**走 `POST /api/miniprogram/upload` + `folder=article-images`**PDF/Office 等附件**走 `folder=book-attachments`**`.txt` / `.md` / `.json` / `.csv` 等**可在端上 `readFile` 后并入请求体 `referenceText`(详见 `开发文档/5、接口/API接口完整文档.md` §1.3)。
- **上传封装**`utils/miniprogramUpload.js`Bearer 与 baseUrl 一致)。
### 2.3 全局状态app.js globalData
- `userInfo`登录后用户信息id、openId、nickname、purchasedSections、hasFullBook、referralCode、referralCount 等)。
@@ -95,6 +122,7 @@ miniprogram/
### 3.4 分享与落地
- 阅读页分享:`onShareAppMessage` / `onShareTimeline``id=章节ID&ref=当前用户邀请码`,落地后 ref 写入 storage绑定与订单归属同上。
- **超级个体动态**(阅读页 `pageMode=super_article`):分享路径为 `/pages/read/read?superArticleId=…`,与章节链路分离。
- 文章/章节分销与全局同一套不按“哪篇文章带来”单独分成或统计仅按“谁发的链接ref=谁)”归属。
---

View File

@@ -73,7 +73,59 @@ Authorization: Bearer admin-token-secret
**接口**: `GET /api/miniprogram/super/articles/{id}`
**说明**:
- 返回文章详情(标题、正文、作者昵称头像、创建时间
- 返回文章详情(标题、正文、配图、作者昵称头像、创建时间、审核状态等,以后端实现为准)。
- 审核中非公开稿件:作者查看须在请求中带 `viewerUserId`(当前用户 openId与 soul-api `MiniprogramSuperArticleDetail` 一致。
### 删除文章(作者本人)
**接口**: `DELETE /api/miniprogram/super/articles/{id}?userId={openId}`
**兼容**: `POST /api/miniprogram/super/articles/{id}/delete?userId=`(语义同 DELETE便于调试
### 小程序落地页(详情 UI
- **统一使用阅读页**`pages/read/read?superArticleId={id}`(兼容 query **`saId`**)。**勿**使用章节参数 `id` 传递动态主键,以免与章节 id 混淆。
- **旧版路径**`pages/super-article-detail/super-article-detail?id=` 仍可用,进入后重定向至上述路径。
- **H5 拉起小程序**:服务端生成的 path 为 `pages/read/read?superArticleId=...`(见 `h5_super_article.go`)。
### AI 生成文章草稿(多文件 + 提示词)
**接口**: `POST /api/miniprogram/super/articles/generate`
**请求体**:
```json
{
"userId": "小程序当前用户 id必填",
"description": "提示词,可选;可与参考素材组合",
"referenceText": "可选,端上读取的纯文本(如多份 .txt/.md 拼接)",
"materialUrls": ["可选,附件上传接口返回的完整 URL"],
"imageUrls": ["可选,图片上传接口返回的公网 URL"]
}
```
**校验**:
- 仅**超级个体**可调(与发布接口同一身份判定)。
- `description``referenceText` 非空、`materialUrls``imageUrls` **四者至少一项有内容**;若仅有图片且无文字素材,服务端会补充默认写作说明再调模型。
**响应**:
```json
{ "success": true, "data": { "title": "...", "content": "..." } }
```
**运行依赖**: 服务端需配置 OpenAI 兼容通道(如 `OPENAI_API_KEY`,及可选 `OPENAI_BASE_URL``OPENAI_MODEL`)。
**小程序对接要点**(页面:`pages/super-article-editor/super-article-editor`:
- 会话内选文件:`wx.chooseMessageFile`(可多选)。
- **图片**`POST /api/miniprogram/upload``formData.folder = article-images`,将返回 URL 填入 `imageUrls`
- **附件**PDF、Office、zip 等):`folder = book-attachments`走附件校验与大小上限与章节附件同源策略URL 填入 `materialUrls`
- **可读文本**`.txt``.md``.json``.csv` 等):可在端上用 `FileSystemManager.readFile` 读入 UTF-8拼进 `referenceText`(过长时端上可先截断,如约 2.8 万字符再提交)。
- 上传建议使用 `utils/miniprogramUpload.js`,请求头携带 `Authorization: Bearer <token>`
- 生成接口耗时长,建议 `wx.request` / `app.request` **timeout ≥ 120000**(毫秒)。
**服务端素材拉取materialUrls**:
- 仅允许拉取 URL 路径含 **`/uploads/`**,且主机为 **配置项 `API_BASE_URL` 对应主机**或 **阿里云 OSS 域名(含 `*.aliyuncs.com`**,用于降低 SSRF 风险。
- 单文件最多读取约 **512KB**;合并后的参考正文在服务端约 **6 万字符**处截断。
- PDF/Office 等二进制文件通常无法在服务端抽出正文,接口会在提示中说明「未解析」;产品侧可引导用户改用 `.txt/.md` 或把要点写在 `description`
---

View File

@@ -7,6 +7,7 @@
| 文档 | 说明 |
|------|------|
| [**API接口完整文档.md**](./API接口完整文档.md) | Soul `/api` **主真源**REST 模块划分、鉴权说明) |
| [**文件上传与OSS.md**](./文件上传与OSS.md) | **路由摘要**OSS/本地策略与运维见 [6、后端/阿里云OSS与文件上传.md](../6、后端/阿里云OSS与文件上传.md) |
| [配置清单-完整版.md](./配置清单-完整版.md) | 环境变量与配置项 |
| [在线支付对接文档.md](./在线支付对接文档.md) | 支付对接 |
| [接口与提现.md](./接口与提现.md) | 提现与相关接口 |

View File

@@ -0,0 +1,23 @@
# 文件上传与 OSS接口侧摘要
> **完整约定与运维排障**:见 [**6、后端/阿里云OSS与文件上传.md**](../6、后端/阿里云OSS与文件上传.md)(以 `internal/oss`、`upload.go` 为准。本节仅列路由与语义避免与后端长文重复。最后更新2026-05-09。
## 路由摘要
| 方法与路径 | 鉴权 | 说明 |
|-----------|------|------|
| `POST /api/upload` | 无硬性要求 | multipart`file``folder` |
| `POST /api/admin/upload` | **Admin JWT** | **soul-admin**`ADMIN_UPLOAD_PATH` |
| `DELETE /api/upload``DELETE /api/admin/upload` | 后者须 Admin | query`path=` |
| `POST /api/miniprogram/upload` | - | 与 `UploadPost` 同源 |
| `POST /api/miniprogram/upload/image``/video` | - | `upload_content`,图可压缩 |
## 行为摘要
**先 OSS`oss_config` 有效),失败或未配置则落本地**;接口多返回 `success: true`。成功时 **`data.storage`**`oss` | `local``/api/upload` 系列)。
## 脚本
- 缺少 `super_articles` 表:`soul-api/scripts/create_super_articles.sql`
返回 [本目录 README](./README.md) · [后端完整说明](../6、后端/阿里云OSS与文件上传.md) · [开发文档索引](../索引.md)

View File

@@ -1,6 +1,6 @@
# Soul创业实验 - API密钥与配置清单
> 最后更新: 2026-01-25
> 最后更新: 2026-05-09补充 OSS 业务文档链接)
> 维护人: 卡若
> ⚠️ 本文件包含敏感信息,请勿公开
@@ -87,6 +87,8 @@
| **AccessKey ID** | `LTAI5t9zkiWmFtHG8qmtdysW` |
| **AccessKey Secret** | `xxjXnZGLNvA2zDkj0aEBSQm3XZAaro` |
**Soul 业务 OSS素材/头像等)**:以库表 `system_config.oss_config` 为主;可选环境变量 `ALIYUN_OSS_ACCESS_KEY_ID` / `ALIYUN_OSS_ACCESS_KEY_SECRET` 在库内 Secret 无效时兜底。详见 [**开发文档/6、后端/阿里云OSS与文件上传.md**](../6、后端/阿里云OSS与文件上传.md)。
---
## 五、数据库

View File

@@ -7,6 +7,7 @@
| 文件 | 说明 |
|------|------|
| [后端架构.md](./后端架构.md) | 服务架构与模块 |
| [**阿里云OSS与文件上传.md**](./阿里云OSS与文件上传.md) | **OSS 配置、环境变量兜底、上传策略、路由与排障**(路由短表互链 [5、接口/文件上传与OSS.md](../5、接口/文件上传与OSS.md) |
| [后端开发规范.md](./后端开发规范.md) | 编码、路由、响应约定 |
| [内容创建问题修复说明.md](./内容创建问题修复说明.md) | 内容创建专项 |
| [soul-admin与Mycontent-temp内容页对比.md](./soul-admin与Mycontent-temp内容页对比.md) | 管理端内容页对比 |

View File

@@ -0,0 +1,86 @@
# 阿里云 OSS 与文件上传soul-api
> **以代码为准**`soul-api/internal/oss`、`internal/handler/upload.go`、`internal/handler/upload_content.go`、`internal/handler/db.go`(系统设置)。
> 最后更新2026-05-09
---
## 1. 配置存哪里
| 来源 | 说明 |
|------|------|
| **主配置** | MySQL 表 `system_config``config_key = 'oss_config'``config_value` 为 JSON |
| **环境变量兜底** | 当库里的 `accessKeySecret` 为空或为占位符(如 `****`、全星号)时,可用 `ALIYUN_OSS_ACCESS_KEY_ID` / `ALIYUN_OSS_ACCESS_KEY_SECRET` 补齐后再校验(见 `soul-api/.env.development` 注释) |
JSON 字段与管理端「系统设置 → OSS」一致常见键名
- `endpoint`:如 `oss-cn-beijing.aliyuncs.com`(可不含 `https://`
- `bucket``region`
- `accessKeyId``accessKeySecret`
- `publicBaseUrl`可选CDN 或自定义域名,`https://` 开头或裸域名皆可
**说明**RAM 控制台下发的 **AccessKey Secret 为长随机串**。若库里误存字面量 `****`,阿里云会返回 **SignatureDoesNotMatch**403与「界面脱敏」不是一回事。
---
## 2. 运行时如何读配置
- 业务统一走 **`internal/oss.LoadConfig()`**:解析 `oss_config`,再应用环境变量兜底,最后校验 bucket / endpoint / 非占位 Secret。
- **`oss.IsEnabled()`**、通用上传、`upload/image``upload/video` 均依赖同一套逻辑。
管理端 **`GET /api/admin/settings`** 中的 `ossConfig`
-**能成功 `LoadConfig()`** 时,回显 **与实际上传一致的生效字段**(含合并环境变量后的密钥),并设 `secretConfigured: true`
-**不能加载有效配置**,则仍以库中原始 JSON 为主,`secretConfigured: false`(常见于库里 Secret 为占位符且未配置环境变量兜底)。
保存:**`POST /api/admin/settings`** 的 `ossConfig` 会合并历史密钥(避免仅因前端未重填 Secret 而覆盖);合并后须能通过 **`oss.ConfigJSONIsReady`** 校验才写入。
---
## 3. 上传策略与路由
**策略(统一)**:若 `LoadConfig()` 成功则 **先上传 OSS**;若 PutObject 失败、返回空 URL**未配置有效 OSS**,则 **自动回退本地磁盘**`uploads/` 等,由 `UPLOAD_DIR` / 默认目录与 Gin `Static /uploads` 提供访问)。处理函数 **不再因 OSS 失败向客户端返回 5xx**(仍 `success: true` 时),便于线上不因 OSS 瞬时故障中断业务;服务端打 **log** 便于排障。响应 JSON 中常见 **`data.storage`**`oss` | `local`
> 说明:配置里曾出现历史环境变量 `UPLOAD_ALLOW_LOCAL_FALLBACK`**handler 已默认本地兜底**,该开关不再控制是否允许回退(详见 `internal/config/config.go` 注释)。
| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| POST | `/api/admin/upload` | 管理端 JWT | 通用 multipart`file` + `folder` |
| DELETE | `/api/admin/upload` | 管理端 JWT | query `path=`,本地路径或 OSS URL |
| POST | `/api/upload` | 无硬性要求 | 与 `UploadPost` 同源 |
| DELETE | `/api/upload` | 视部署 | 同上 |
| POST | `/api/miniprogram/upload` | 小程序侧约定 | 同源 `UploadPost` |
| POST | `/api/miniprogram/upload/image` | 同上 | 图片,可压缩;内部 `ossUploadBytes` 再本地 |
| POST | `/api/miniprogram/upload/video` | 同上 | 视频;内部 `ossUploadFile` 再本地 |
小程序封装见 `miniprogram/utils/miniprogramUpload.js`。**接口侧短表**见 [5、接口/文件上传与OSS.md](../5、接口/文件上传与OSS.md)。
---
## 4. 本地与排障
### 4.1 自检脚本
仓库 **`soul-api/scripts/test_oss_connect.py`**(依赖 `pip install oss2`
- 默认读取脚本内示例 JSON**Secret 占位时必须设置环境变量** `OSS_ACCESS_KEY_SECRET``ALIYUN_OSS_ACCESS_KEY_SECRET`
- 成功则对目标 Bucket 执行 `list_objects(max_keys=1)`
### 4.2 常见现象
| 现象 | 可能原因 |
|------|----------|
| `secretConfigured: false` | 库内 Secret 无效/占位;或未设环境变量兜底 |
| 上传提示未配置 OSS | `LoadConfig()` 失败,查日志 `oss: oss_config rejected` / `parse error` |
| 管理端 Network 里 Secret 仍是 `****` | 多为 **库里就是字面量 `****`**;修好库或配置 env 并重启后,若 `LoadConfig` 成功GET settings 会回显生效密钥 |
---
## 5. 安全与运维
- **不要将含真实 OSS Secret 的 `.env` 提交到公开仓库。**
- 管理端返回完整密钥依赖 **Admin 鉴权**;若接口暴露在公网,须配合 HTTPS、IP 限制与强口令。
---
返回 [6、后端 README](./README.md) · [开发文档索引](../索引.md)

View File

@@ -0,0 +1,50 @@
智增增 OpenAI 兼容 API超级个体 AI 写文章)
基地址https://api.zhizengzeng.com/v1
接口POST /chat/completions与 OpenAI 一致)
开发文档(模型与价格):
https://doc.zhizengzeng.com/doc-3979947
密钥:请在智增增控制台创建,写入 soul-api 环境变量(勿明文存入仓库):
- SUPER_ARTICLE_AI_API_KEY推荐与其他 OPENAI 用途隔离)
- 或 OPENAI_API_KEY
- SUPER_ARTICLE_AI_MODEL默认文本与配图均可优先使用如 gpt-4o-mini支持 vision
- SUPER_ARTICLE_AI_VISION_MODEL有参考图时自动改用、与图片一并分析的多模态模型默认 gpt-4o-mini需在控制台支持 vision
完整示例见仓库soul-api/.env.super-article-ai.example
---
以下「写文章规则提示词」由服务端在调用大模型前自动追加在用户提示词与参考素材之后(无须小程序重复传);若需修改规则,请同步改 soul-api/internal/handler/miniprogram_ai_article.go 常量 superArticleWritingRulesSuffix。
写文章规则提示词:
写文章。严格按以下规则成稿。
【材料】用户提供:本场聊天记录/纪要/妙记要点、场次号、主题短句;写作前先按规范阅读飞书运营报表,把报表上有的时长、场观、进房等具体数字写进开篇(只写一次)。
【人称与禁用】叙述全程用「我」,禁止「房主」。禁止「这边」「那边」。禁止「回答说,……」——问句后直接接我的陈述。语气偏强势:干脆、笃定,少「可能/也许」。「卡若」每篇最多 1 次,可完全不出现。
【刺点三对齐】正文第一行标题「# 第X场短句」、磁盘文件名、开篇刺激性金句三者同一刺点不写空泛抒情。
【结构与比例】
- 默认单场 采用「2026 每日派对干货 / 派对体」:三块约 40% / 20%30% / 余下约 30%块与块之间必须有过渡口语每一块结束必须有一段「段末小结」单独成段3050 个汉字一句,禁止出现「干货」等标签字。
【开篇】先有冲突或世俗金句,再切入场景。固定数据句式(报表有则写具体数,没有的项不写):「第 X 场X 分钟;场观 XXXXX进房 XXX 人。」不写「约」、不写「见报表」。时长/场观/进房仅在开篇出现一次。
【中段】推进用时间线或事件线多写对话「X 号问」「有人问」);我的话直接陈述。约在全文 50% 处插入一段浓缩要点 36 条2040 字/条),前后用 --- 分隔,条目中禁止「干货」二字。
【分享句】约在 20% 处一句、结尾一句,各 ≤50 字,紧扣本节,禁止「干货:」字样。
【强调·派对体成稿必选】Markdown **加粗**:至少 2 句完整立场/结论句(分布在不同段落);至少 3 个不同的重点关键词加粗。禁止堆砌无意义加粗。
【结尾】升华主题派对体可有文末小结36 条短金句)+ 总结段落 + 一句收尾金句。
【字数与真实性】小节 30005000 字;忠于来源不改原意;所有数字必须有依据、写具体算法/金额/人次等Soul 侧常用口径可参考规范中的曝光/进房/成本写法。
【隐晦植入】文中自然散布 24 处联系管理、做切片、做副业相关一句不单段喊话随机10%加率不用每篇文章都加
【版式强制】一句一行,句间空一行;分号也拆成行;段落之间空行;大段堆砌禁止。
【交付】输出 Markdown标题一行「# 第X场…」成稿末尾按模板追加 HTML 注释自检:「我」字数/次数、本场关键字频次、关键字加粗处数、重点句/词加粗是否达标。

View File

@@ -133,6 +133,10 @@
/api/miniprogram/ckb/join
/api/miniprogram/ckb/match ← 已挂载
/api/miniprogram/upload
/api/miniprogram/my/super-stats
/api/miniprogram/super/articles ← GET 列表、POST 发布
/api/miniprogram/super/articles/:id ← GET 详情
/api/miniprogram/super/articles/generate ← POST AI 生成草稿(参考 开发文档/5、接口/API接口完整文档.md §1.3
/api/miniprogram/user/addresses, addresses/:id
/api/miniprogram/user/check-purchased, profile, purchase-status, reading-progress, update
/api/miniprogram/withdraw, withdraw/records, pending-confirm, confirm-received, confirm-info

View File

@@ -1,6 +1,6 @@
# Soul 创业派对 - 开发文档索引
> **以代码为准**:需求或实现变更时同步更新本文档与对应章节 README。最后更新2026-04-07
> **以代码为准**:需求或实现变更时同步更新本文档与对应章节 README。最后更新2026-05-09
---
@@ -99,7 +99,7 @@
| [API接口完整文档.md](./5、接口/API接口完整文档.md) | **Soul 项目 API 主真源** |
| [配置清单-完整版.md](./5、接口/配置清单-完整版.md) | 配置项清单 |
| [在线支付对接文档.md](./5、接口/在线支付对接文档.md) | 支付 |
| [接口与提现.md](./5、接口/接口与提现.md) | 提现相关接口说明 |
| [文件上传与OSS.md](./5、接口/文件上传与OSS.md) | 路由摘要,`data.storage`,互链后端详版 |
---
@@ -108,6 +108,7 @@
| 文件/路径 | 用途 |
|-----------|------|
| [README.md](./6、后端/README.md) | 本目录索引 |
| [**阿里云OSS与文件上传.md**](./6、后端/阿里云OSS与文件上传.md) | **OSS、上传策略、路由、env 兜底、`storage`、`UPLOAD_*`、自检脚本** |
| [后端架构.md](./6、后端/后端架构.md) | soul-api 架构 |
| [后端开发规范.md](./6、后端/后端开发规范.md) | 编码与路由约定 |
| [内容创建问题修复说明.md](./6、后端/内容创建问题修复说明.md) | 专项修复说明 |