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:
@@ -52,9 +52,10 @@
|
||||
| 2026-04-13 | 会议:按功能同步开发文档;页面级记 miniprogram 接口与分享行为;无接口变更亦标注 | 进行中 |
|
||||
| 2026-04-14 | 吸收沉淀:逆推闭环自检写入 evolution;LOOP 需求行 | 进行中 |
|
||||
| 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
|
||||
|
||||
@@ -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 已更新
|
||||
|
||||
@@ -77,7 +77,7 @@ description: Soul 创业派对变更关联检查。miniprogram/soul-admin/soul-a
|
||||
| 找伙伴/匹配 | 匹配、展示 | match、ckb 等 miniprogram 接口 | 匹配配置、开关 |
|
||||
| 配置项 | 仅读取 | miniprogram/config 或 db/config | 配置编辑(admin/db) |
|
||||
| VIP/超级个体 | 展示列表、详情 | miniprogram/vip/members;db/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` 混表或混接口。
|
||||
|
||||
|
||||
@@ -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)。
|
||||
- 做阅读、文章等需长按复制的文本时(遵循 §9,text 加 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 接口混用。
|
||||
|
||||
@@ -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,避免与现有表职责混淆。
|
||||
|
||||
@@ -15,8 +15,8 @@ function newAttachId() {
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 临时隐藏「AI 写文章」卡片;需上线时再设为 true */
|
||||
showAiWriteSection: false,
|
||||
/** 「AI 帮你写」卡片:参考图 + 提示词 + 生成 */
|
||||
showAiWriteSection: true,
|
||||
statusBarHeight: 44,
|
||||
auditMode: false,
|
||||
title: '',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
},
|
||||
"enablePullDownRefresh": false,
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 160,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#0b1220"
|
||||
|
||||
12
soul-api/.env.super-article-ai.example
Normal file
12
soul-api/.env.super-article-ai.example
Normal 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
|
||||
@@ -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 将路径拼接到 BaseURL,path 应以 / 开头
|
||||
@@ -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)强制校验敏感配置,禁止使用默认值
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -22,15 +22,47 @@ import (
|
||||
|
||||
const aiArticleSystemPrompt = `你是「卡若创业派对」的内容创作助手,帮助超级个体写出真实、有深度的商业与创业内容文章。
|
||||
|
||||
写作要求:
|
||||
- 标题:精炼有力,≤30字,有吸引力,点明核心价值
|
||||
- 正文:600-1000字,有真实故事或案例,有观点洞察,语气自然真诚
|
||||
- 风格:像朋友分享经验,而非说教;有画面感,接地气
|
||||
- 不要使用"首先、其次、最后"等模板化结构词
|
||||
若用户提供的素材末尾附有「写文章规则提示词」或以「---」分隔的写作规范段落,你必须优先、严格按该段落成稿(体裁、字数、人称、版式、Markdown 交付与自检等)。
|
||||
|
||||
严格输出 JSON,不要有任何额外文字:
|
||||
输出封装(始终遵守):只输出一段合法 JSON,不要有任何 JSON 外的文字。title 为主标题纯文本(可与正文首行 Markdown 标题语义一致,但不要带「#」前缀);content 为正文全文,可为 Markdown(含 **加粗**、分段、---、HTML 注释等),字符串内需合法 JSON 转义。
|
||||
|
||||
若用户未附带上述写作规范段落,则默认:标题精炼有力≤30字;正文约600~1000字;语气自然真诚,像朋友分享经验;不要使用"首先、其次、最后"等模板化结构词。
|
||||
|
||||
严格输出 JSON:
|
||||
{"title":"文章标题","content":"文章正文"}`
|
||||
|
||||
// superArticleWritingRulesSuffix 在服务端拼接到用户素材末尾(与 开发文档/ai开发文档.txt 写作规则段落保持一致)。
|
||||
const superArticleWritingRulesSuffix = `写文章规则提示词:
|
||||
|
||||
写文章。严格按以下规则成稿。
|
||||
|
||||
【材料】用户提供:本场聊天记录/纪要/妙记要点、场次号、主题短句;写作前先按规范阅读飞书运营报表,把报表上有的时长、场观、进房等具体数字写进开篇(只写一次)。
|
||||
|
||||
【人称与禁用】叙述全程用「我」,禁止「房主」。禁止「这边」「那边」。禁止「回答说,……」——问句后直接接我的陈述。语气偏强势:干脆、笃定,少「可能/也许」。「卡若」每篇最多 1 次,可完全不出现。
|
||||
|
||||
【刺点三对齐】正文第一行标题「# 第X场|短句」、磁盘文件名、开篇刺激性金句三者同一刺点;不写空泛抒情。
|
||||
|
||||
【结构与比例】
|
||||
- 默认单场 采用「2026 每日派对干货 / 派对体」:三块约 40% / 20%~30% / 余下约 30%;块与块之间必须有过渡口语;每一块结束必须有一段「段末小结」单独成段,30~50 个汉字一句,禁止出现「干货」等标签字。
|
||||
|
||||
【开篇】先有冲突或世俗金句,再切入场景。固定数据句式(报表有则写具体数,没有的项不写):「第 X 场,X 分钟;场观 XXXXX,进房 XXX 人。」不写「约」、不写「见报表」。时长/场观/进房仅在开篇出现一次。
|
||||
|
||||
【中段】推进用时间线或事件线;多写对话(「X 号问」「有人问」);我的话直接陈述。约在全文 50% 处插入一段浓缩要点 3~6 条(20~40 字/条),前后用 --- 分隔,条目中禁止「干货」二字。
|
||||
|
||||
【分享句】约在 20% 处一句、结尾一句,各 ≤50 字,紧扣本节,禁止「干货:」字样。
|
||||
|
||||
【强调·派对体成稿必选】Markdown **加粗**:至少 2 句完整立场/结论句(分布在不同段落);至少 3 个不同的重点关键词加粗。禁止堆砌无意义加粗。
|
||||
|
||||
【结尾】升华主题;派对体可有文末小结(3~6 条短金句)+ 总结段落 + 一句收尾金句。
|
||||
|
||||
【字数与真实性】小节 3000~5000 字;忠于来源不改原意;所有数字必须有依据、写具体算法/金额/人次等,Soul 侧常用口径可参考规范中的曝光/进房/成本写法。
|
||||
|
||||
【隐晦植入】文中自然散布 2~4 处:联系管理、做切片、做副业相关一句,不单段喊话,随机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 content(text + 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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=谁)”归属。
|
||||
|
||||
---
|
||||
|
||||
@@ -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`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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) | 提现与相关接口 |
|
||||
|
||||
23
开发文档/5、接口/文件上传与OSS.md
Normal file
23
开发文档/5、接口/文件上传与OSS.md
Normal 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)
|
||||
@@ -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)。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库
|
||||
|
||||
@@ -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) | 管理端内容页对比 |
|
||||
|
||||
86
开发文档/6、后端/阿里云OSS与文件上传.md
Normal file
86
开发文档/6、后端/阿里云OSS与文件上传.md
Normal 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)
|
||||
50
开发文档/ai开发文档.txt
Normal file
50
开发文档/ai开发文档.txt
Normal 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%;块与块之间必须有过渡口语;每一块结束必须有一段「段末小结」单独成段,30~50 个汉字一句,禁止出现「干货」等标签字。
|
||||
|
||||
【开篇】先有冲突或世俗金句,再切入场景。固定数据句式(报表有则写具体数,没有的项不写):「第 X 场,X 分钟;场观 XXXXX,进房 XXX 人。」不写「约」、不写「见报表」。时长/场观/进房仅在开篇出现一次。
|
||||
|
||||
【中段】推进用时间线或事件线;多写对话(「X 号问」「有人问」);我的话直接陈述。约在全文 50% 处插入一段浓缩要点 3~6 条(20~40 字/条),前后用 --- 分隔,条目中禁止「干货」二字。
|
||||
|
||||
【分享句】约在 20% 处一句、结尾一句,各 ≤50 字,紧扣本节,禁止「干货:」字样。
|
||||
|
||||
【强调·派对体成稿必选】Markdown **加粗**:至少 2 句完整立场/结论句(分布在不同段落);至少 3 个不同的重点关键词加粗。禁止堆砌无意义加粗。
|
||||
|
||||
【结尾】升华主题;派对体可有文末小结(3~6 条短金句)+ 总结段落 + 一句收尾金句。
|
||||
|
||||
【字数与真实性】小节 3000~5000 字;忠于来源不改原意;所有数字必须有依据、写具体算法/金额/人次等,Soul 侧常用口径可参考规范中的曝光/进房/成本写法。
|
||||
|
||||
【隐晦植入】文中自然散布 2~4 处:联系管理、做切片、做副业相关一句,不单段喊话,随机10%加率不用每篇文章都加
|
||||
|
||||
【版式强制】一句一行,句间空一行;分号也拆成行;段落之间空行;大段堆砌禁止。
|
||||
|
||||
【交付】输出 Markdown;标题一行「# 第X场|…」;成稿末尾按模板追加 HTML 注释自检:「我」字数/次数、本场关键字频次、关键字加粗处数、重点句/词加粗是否达标。
|
||||
@@ -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
|
||||
|
||||
@@ -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) | 专项修复说明 |
|
||||
|
||||
Reference in New Issue
Block a user