重构:更新API基础URL并增强内容解析逻辑

- 更改了API基础URL,以便在开发和生产环境之间切换。
- 添加了assetBase参数,以提高内容解析中图像源的分辨率。
- 增强了阅读页面的配置,将assetBase纳入图像URL的配置中。
- 更新了OpenPlatformPage,以便正确导航至设置和API文档。
- 对ContentPage进行了重构,以包含额外的webhook配置检索功能。
- 改进了DashboardPage,使其能更有效地处理用户交互。
- 清理了未使用的导入,并优化了AdminLayout,以提高可读性。
This commit is contained in:
Alex-larget
2026-04-02 17:15:03 +08:00
parent a696b0cefc
commit 54955f5512
20 changed files with 379 additions and 110 deletions

View File

@@ -0,0 +1,88 @@
# 智能体技能&经验总索引
用途:让任何智能体在“开始实现或回答”之前,先快速匹配“有没有相关经验”和“应该用哪个 Skill”再综合处理。
---
## 1. 经验索引(按角色)
- 助理橙子:`agent/开发助理/evolution/索引.md`
- 2026-03-21MBTI 头像小程序全链路兜底 + 匹配接口回填
- 2026-03-16用户交互习惯分析基于 agent-transcripts
- 小程序开发工程师:`agent/小程序开发工程师/evolution/索引.md`
- 2026-03-30阅读页 `navigateToMiniProgram` 时 path 追加 `phone`
- 2026-03-17代付美团式流程与目录 loading/折叠
- 2026-03-16编辑资料页分享名片Canvas 封面)
- 管理端开发工程师:`agent/管理端开发工程师/evolution/索引.md`
- 2026-03-16链接人与事复制/删除 Dialog、planId/apiKey 列)
- 2026-03-10迁移 Mycontent-temp主导航收敛/Settings 承载)
- 2026-03-05文章详情@某人(编辑页插入 @ 用户)
- 后端工程师:`agent/后端工程师/evolution/索引.md`
- 2026-03-24router 缺失四 handler 补齐BookRanking 等)
- 2026-03-17PayNotify 权益归属发起人
- 2026-03-14内容排名算法修正rank 分公式)
- 产品经理:`agent/产品经理/evolution/索引.md`
- 2026-03-24开发进度同步会议文档同步原则/项目索引补齐)
- 2026-03-10需求同步管理端迁移入口策略
- 2026-03-05文章详情@某人(验收标准与待确认)
- 软件测试:`agent/软件测试/evolution/索引.md`
- 2026-03-16scripts/test/process 流程测试目录
- 2026-03-05文章详情@某人(用例、联调与回归点)
- 2026-03-10管理端迁移回归鉴权/跳转可达性)
- 安全工程师:`agent/安全工程师/evolution/索引.md`
- 2026-03-20挖矿病毒排查经验转化为 Skills
- 2026-03-20部署 API / 部署触发词(环境与脚本映射)
- 团队(跨角色共享):`agent/团队/evolution/索引.md`
- 2026-03-16TipTap Mention 需 data-label
- 2026-03-17代付美团式流程与权益归属约定
- 2026-03-24文档同步原则实现变更后同步需求/运营/索引)
- 老板分身:`agent/老板分身/evolution/索引.md`
---
## 2. 技能索引(按任务类型/触发)
- 小程序开发WXML/WXSS/JS、`/api/miniprogram/*``skills/miniprogram-dev/SKILL.md`
- 触发:`miniprogram/`、小程序、微信原生、C 端能力改动
- 管理端开发React/Vite、`/api/admin/*``/api/db/*`、禁止 `alert``skills/admin-dev/SKILL.md`
- 触发:`soul-admin/`、管理端、React 后台、调用 admin/db 接口
- 后端开发Go/Gin/GORM、路由分组`skills/api-dev/SKILL.md`
- 触发:`soul-api`、API、Go、GORM、新增接口/路由分组
- 产品经理(需求分析/验收/加个需求):`skills/product-manager/SKILL.md`
- 触发:需求分析、需求文档、验收、加个需求
- 测试(功能/回归/三端联调):`skills/testing/SKILL.md`
- 触发测试、测试用例、回归测试、QA
- 助理橙子(文档同步/经验入库/会议结束/日志):`skills/assistant-doc-sync/SKILL.md`
- 触发:小橙/橙子/橙橙/🍊、讨论完毕、吸收经验、保存进度、会议结束、出日志
- 团队会议(多角色发言、决议、纪要):`skills/team-meeting/SKILL.md`
- 触发:开个会、团队会议、需求评审、方案讨论
- 变更关联检查(防漏改,改完必过):`skills/change-checklist/SKILL.md`
- 触发:变更完成、检查一下、准备提交
- 跨端协同流程(职责分工/联想补齐):`skills/role-flow-control/SKILL.md`
- 触发:跨端功能开发、三端联动优化
- 三端架构分析/迁移拆解约束:`skills/three-tier-arch/SKILL.md`
- 触发:拆解、迁移、框架选择、三端架构
- 新版快速分析(甲方/第三方改版补齐逻辑与接口规范):`skills/new-version-analyze/SKILL.md`
- 触发:新版分析、版本对比、迁移分析、甲方代码分析
- next-project 仅预览说明:`skills/next-preview/SKILL.md`
- 触发:区分线上后端与 next-project 预览
- Next.js 拆解为前后端分离+小程序:`skills/next-split/SKILL.md`
- 触发Next 全栈拆解、架构迁移
- MySQL 直连操作MCP 不通时执行 DDL/DML`skills/mysql-direct/SKILL.md`
- 触发数据库操作、增减字段、紧急迁移、MCP 连接失败
- 挖矿病毒排查与防护xmrig/miner_guard 等):`skills/security-miner-guard/SKILL.md`
- 触发挖矿病毒、xmrig、服务器被入侵、杀挖矿
- 服务器操作与部署SSH/宝塔/Cunkebao`skills/security-server-ops/SKILL.md`
- 触发部署、服务器操作、SSH、devloy、master
- 卡若创业派对运营(报表/切片/分发/飞书视频文字下载):`skills/karuo-party/SKILL.md`
- 触发:运营报表、视频切片、多平台分发、飞书视频下载、派对运营
---
## 3. 使用流程(任何智能体都照做)
1. 先归类:根据编辑目录或触发词判断“可能涉及哪些角色/任务类型”。
2. 再查经验:打开对应角色的 `evolution/索引.md`,找最近且最贴近的经验条目。
3. 再查技能:打开对应 Skill 的 `SKILL.md`,按其执行流程/约束落地。
4. 再综合:若命中多个条目,按“边界规则 + 变更闭环(如 change-checklist+ 必要的联动(如会议/跨端)”合并决策。

View File

@@ -0,0 +1,26 @@
---
name: vip-members Person 聚合 + 文章绑定超级个体
overview: 见 Cursor 计划「vip-members_person_聚合」含文章绑定超级个体与白名单 Webhook。执行前以本文件与 Cursor plan 同步为准。
---
完整内容与 todos 见 IDE 内计划:`vip-members_person_聚合_2b9cac41.plan.md`(或从命令面板打开 Current Plan
要点摘要:
1. **原有三项**vip-members 聚合 Person、UsersPage @列、ContentPage `?tab=link-person`
2. **新增**`chapters.bound_super_user_id`;管理端章节绑定超级个体;读文接口返回绑定信息。
3. **Webhook**:仅**白名单**埋点(分享/朋友圈/购买/单页解锁引导 + 补 `view_chapter``MiniprogramTrackPost` 后推送,使用 `loadLeadWebhookURL`**独立去重**勿与获客日去重混用。
4. **留资**`ckb/lead` 按章节绑定合并 `TargetMemberID`,新增来源文案。
用户已选推送策略:**白名单关键事件**(非全量节流、非定时摘要)。
---
## 补充:找伙伴(终身 1 次免费 + ¥9.9 划线 / ¥1
- **规则与 UI**:后端 `match.go` 与小程序 `match` 页已基本实现「终身 1 次免费、不按天重置」及划线原价 + 现价展示。
- **支付对齐**`getStandardPrice(match)` 当前误读 `chapter_config` 且兜底 **¥68**,需改为读 **`match_config.matchPrice`**、兜底 **¥1**(详见 Cursor 主计划文末章节与 todo `match-quota-pay-align`)。
- **#标签·内部跳转**:与现有 `type=internal`管理端现名「本小程序页面」为同一能力执行时可将文案统一为「内部跳转」todo `linktag-internal-label`)。
- **行为/Webhook 中文化**:飞书「最近行为」里英文 action、英文 module、技术 target 串,需在 `user.go` 补映射与人话化 targettodo `track-webhook-cn-labels`)。

View File

@@ -0,0 +1,17 @@
---
description: 所有智能体处理任务前先查经验与技能索引
globs: ["**"]
alwaysApply: true
---
# 强制流程:先查经验与技能再处理
当任何智能体接到“问题/需求/变更/执行”任务时,必须按以下顺序完成(不能跳过):
1. 归类角色:根据编辑目录或触发词判断可能涉及的角色/任务类型。
2. 查总索引:先打开 `.cursor/agent/智能体-技能-经验总索引.md` 找匹配的“经验索引文件”和“主 Skill 文件”。
3. 查经验+查 Skill对每个匹配条目先读对应角色 `agent/*/evolution/索引.md`,再按边界规则读取对应主 `skills/*/SKILL.md` 完整内容。
4. 再综合落地:确认是否还需 `change-checklist`、`team-meeting`、`role-flow-control` 等跨层 Skill 后,再回答/执行。
注:本规则不替代现有 project boundary 的“必须 Read 主 Skill 完整内容”的约束,只是增加“先查索引再读 Skill”的前置步骤。

View File

@@ -12,8 +12,8 @@ const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
// baseUrl 手动切换(注释方式):
// const API_BASE_URL = 'http://localhost:9090'
// const API_BASE_URL = 'https://soulapi.quwanzhi.com'
const API_BASE_URL = 'https://souldev.quwanzhi.com'
const API_BASE_URL = 'https://soulapi.quwanzhi.com'
// const API_BASE_URL = 'https://souldev.quwanzhi.com'
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
/** 新用户未完成头像+昵称前,强制留在资料引导(与 avatar-nickname 页配合) */

View File

@@ -71,7 +71,8 @@ function getContentParseConfig() {
aliases: p.aliases != null ? String(p.aliases) : '',
}))
const linkTags = Array.isArray(g.linkTagsConfig) ? g.linkTagsConfig : []
return { persons, linkTags }
const assetBase = String(g.baseUrl || '').replace(/\/$/, '')
return { persons, linkTags, assetBase }
}
/** 补全 mentionDisplay避免旧数据无字段昵称去空白防「@ 名」 */

View File

@@ -27,6 +27,35 @@ function decodeEntities(str) {
.replace(/'/g, "'")
}
/**
* 正文里 img 的 src管理端常见为 /uploads/...(相对 API 根),浏览器会自动补全域名;
* 小程序 <image> 不会拼接 baseUrl需在此用 assetBase 转成可请求的绝对地址。
* @param {string} src
* @param {string} [assetBase] - 如 https://soulapi.example.com无末尾 /
*/
function resolveArticleImageSrc(src, assetBase) {
if (!src || typeof src !== 'string') return src
const s = src.trim()
if (!s) return s
if (/^(https?:|wxfile:|data:|blob:)/i.test(s)) return s
if (s.startsWith('//')) return 'https:' + s
if (s.startsWith('/')) {
const base = String(assetBase || '').replace(/\/$/, '')
if (!base) return s
return base + s
}
return s
}
/** 从 <img ...> 标签取出 src双引号/单引号) */
function extractImgSrcFromTag(tag) {
const d = tag.match(/src\s*=\s*"([^"]*)"/i)
if (d && d[1] != null) return d[1]
const q = tag.match(/src\s*=\s*'([^']*)'/i)
if (q && q[1] != null) return q[1]
return ''
}
/**
* 单行展示用:昵称、#标签文案、章节外标题类字段 — 合并换行、<br>、连续空白(避免 TipTap/粘贴带入异常断行)
*/
@@ -118,11 +147,16 @@ function parseBlockToSegments(block, config) {
segs.push({ type: 'linkTag', label: label || '#', url, tagType: '', pagePath: '', tagId: '' })
} else if (/^<img /i.test(tag)) {
// 图片
const srcMatch = tag.match(/src="([^"]*)"/)
const altMatch = tag.match(/alt="([^"]*)"/)
if (srcMatch) {
segs.push({ type: 'image', src: srcMatch[1], alt: altMatch ? altMatch[1] : '' })
// 图片src 可能为相对路径,需结合 config.assetBase
const rawSrc = extractImgSrcFromTag(tag)
const altMatch = tag.match(/alt\s*=\s*"([^"]*)"/i) || tag.match(/alt\s*=\s*'([^']*)'/i)
if (rawSrc) {
const decoded = decodeEntities(rawSrc)
const src =
config && config.assetBase
? resolveArticleImageSrc(decoded, config.assetBase)
: resolveArticleImageSrc(decoded, '')
segs.push({ type: 'image', src, alt: altMatch ? decodeEntities(altMatch[1]) : '' })
}
}
@@ -139,7 +173,7 @@ function parseBlockToSegments(block, config) {
/**
* 从 HTML 中解析出 lines纯文本行和 segments含富文本片段
* @param {string} html
* @param {object} [config] - { persons: [], linkTags: [] },用于对 text 段自动匹配 @人名 / #标签
* @param {object} [config] - { persons: [], linkTags: [], assetBase?: string },用于对 text 段自动匹配 @人名 / #标签assetBase 用于补全图片相对 URL
*/
function parseHtmlToSegments(html, config) {
const lines = []

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Soul创业派对</title>
<script type="module" crossorigin src="/assets/index-zk0czXxa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CuUAjfcM.css">
<script type="module" crossorigin src="/assets/index-C_CXp_Jv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Gzc8q2lr.css">
</head>
<body>
<div id="root"></div>

View File

@@ -19,7 +19,6 @@ import { MentorsPage } from './pages/mentors/MentorsPage'
import { MentorConsultationsPage } from './pages/mentor-consultations/MentorConsultationsPage'
import { FindPartnerPage } from './pages/find-partner/FindPartnerPage'
import { ApiDocsPage } from './pages/api-docs/ApiDocsPage'
import { OpenPlatformPage } from './pages/open-platform/OpenPlatformPage'
import { NotFoundPage } from './pages/not-found/NotFoundPage'
function App() {
@@ -49,7 +48,7 @@ function App() {
<Route path="match-records" element={<MatchRecordsPage />} />
<Route path="api-doc" element={<Navigate to="/api-docs" replace />} />
<Route path="api-docs" element={<ApiDocsPage />} />
<Route path="open-platform" element={<OpenPlatformPage />} />
<Route path="open-platform" element={<Navigate to="/settings?tab=open-platform" replace />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>

View File

@@ -8,7 +8,6 @@ import {
Wallet,
BookOpen,
GitMerge,
Globe2,
} from 'lucide-react'
import { get, post } from '@/api/client'
import { clearAdminToken, getAdminToken } from '@/api/auth'
@@ -123,17 +122,6 @@ export function AdminLayout() {
<Settings className="w-5 h-5 shrink-0" />
<span className="text-sm"></span>
</Link>
<Link
to="/open-platform"
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
location.pathname === '/open-platform'
? 'bg-[#38bdac]/20 text-[#38bdac] font-medium'
: 'text-gray-400 hover:bg-gray-700/50 hover:text-white'
}`}
>
<Globe2 className="w-5 h-5 shrink-0" />
<span className="text-sm">API开放平台</span>
</Link>
</div>
</nav>

View File

@@ -592,12 +592,24 @@ export function ContentPage() {
isPinned?: boolean
}
type VipRow = { id: string; webhookUrl?: string | null }
const [data, vipRes] = await Promise.all([
const [data, vipRes, whCfg] = await Promise.all([
get<{ success?: boolean; persons?: PersonResp[] }>('/api/db/persons'),
get<{ success?: boolean; data?: VipRow[]; error?: string }>('/api/db/vip-members?limit=500').catch(() => null),
get<{ success?: boolean; data?: Record<string, unknown> }>(
'/api/db/config/full?key=super_individual_webhook_map',
{ cache: 'no-store' as RequestCache },
).catch(() => null),
])
const whMap: Record<string, string> = {}
if (vipRes?.success && Array.isArray(vipRes.data)) {
// 与 DBVipMemberWebhookSet 一致:完整映射存在 system_config勿仅从 vip-members 列表合并
// (列表仅含当前有效 VIP过期会员绑定的人物会导致弹窗空白
if (whCfg?.success && whCfg.data && typeof whCfg.data === 'object' && !Array.isArray(whCfg.data)) {
for (const [k, v] of Object.entries(whCfg.data)) {
const uid = String(k || '').trim()
const w = v != null ? String(v).trim() : ''
if (uid && w) whMap[uid] = w
}
} else if (vipRes?.success && Array.isArray(vipRes.data)) {
for (const m of vipRes.data) {
const uid = (m.id || '').trim()
if (!uid) continue
@@ -2826,7 +2838,7 @@ export function ContentPage() {
@ <span className="text-gray-400">token</span> @ token /
</p>
<p>
<span className="text-purple-300"></span> @ plan/apiKey
<span className="text-purple-300"></span> VIP/ @
</p>
<p>
<span className="text-[#38bdac]"> Webhook</span>线 Webhook

View File

@@ -390,8 +390,19 @@ export function DashboardPage() {
const t = target.trim()
const lower = t.toLowerCase()
// 首页卡片trackClick('home','card_click','超级个体_'+userId) — 中文前缀,不能依赖 lower.includes('super')
if (/^超级个体[_:]/.test(t)) {
const raw = t.replace(/^超级个体[_:]/, '').trim()
const vipName = resolveVipNameByToken(raw) || resolveVipNameByToken(normalizeTrackToken(raw))
if (vipName) return `超级个体:${vipName}`
return `超级个体:${normalizeTrackToken(raw) || raw || '未知'}`
}
if (/^链接头像[_-]/.test(t)) {
const rawName = normalizeTrackToken(t.replace(/^链接头像[_-]/, ''))
const rawId = t.replace(/^链接头像[_-]/, '').trim()
const nick = resolveVipNameByToken(rawId) || resolveVipNameByToken(normalizeTrackToken(rawId))
if (nick) return `头像:${nick}`
const rawName = normalizeTrackToken(rawId)
return rawName ? `头像:${rawName}` : '头像点击'
}
if (/^member[_-]?detail$/i.test(lower) || lower.includes('member detail')) return '成员详情'

View File

@@ -8,9 +8,15 @@ import { OpenPlatformKeysTab } from './OpenPlatformKeysTab'
import { OpenPlatformDocsTab } from './OpenPlatformDocsTab'
import { OpenPlatformLogsTab } from './OpenPlatformLogsTab'
export function OpenPlatformPage() {
export function OpenPlatformPage({ embedded = false }: { embedded?: boolean }) {
return (
<div className="p-8 w-full bg-[#0a1628] text-white min-h-full">
<div
className={
embedded
? 'w-full text-white'
: 'p-8 w-full bg-[#0a1628] text-white min-h-full'
}
>
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-white">API </h1>
@@ -20,7 +26,7 @@ export function OpenPlatformPage() {
</p>
</div>
<Link
to="/api-docs"
to="/settings?tab=api-docs"
className="text-sm text-[#38bdac] hover:underline inline-flex items-center gap-1 shrink-0"
>
<Link2 className="w-4 h-4" />

View File

@@ -41,11 +41,13 @@ import {
LayoutGrid,
Sparkles,
MessageSquare,
Globe2,
} from 'lucide-react'
import { get, post } from '@/api/client'
import { AuthorSettingsPage } from '@/pages/author-settings/AuthorSettingsPage'
import { AdminUsersPage } from '@/pages/admin-users/AdminUsersPage'
import { ApiDocsPage } from '@/pages/api-docs/ApiDocsPage'
import { OpenPlatformPage } from '@/pages/open-platform/OpenPlatformPage'
import { MpUiPopupTableSection } from '@/pages/settings/MpUiPopupTableSection'
import type { PagePopupItem } from '@/pages/settings/mpUiCopyConfig'
import { buildMpUiPayload, splitMpUiForPopupEditor } from '@/pages/settings/mpUiCopyConfig'
@@ -123,7 +125,7 @@ const defaultFeatures: FeatureConfig = {
aboutEnabled: true,
}
const TAB_KEYS = ['system', 'author', 'admin', 'api-docs'] as const
const TAB_KEYS = ['system', 'author', 'admin', 'open-platform', 'api-docs'] as const
type TabKey = (typeof TAB_KEYS)[number]
const SYSTEM_SECTION_KEYS = ['basic', 'mp', 'mp-copy', 'oss', 'features'] as const
@@ -368,6 +370,13 @@ export function SettingsPage() {
<ShieldCheck className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger
value="open-platform"
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
>
<Globe2 className="w-4 h-4 mr-2" />
API
</TabsTrigger>
<TabsTrigger
value="api-docs"
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
@@ -956,6 +965,10 @@ export function SettingsPage() {
<AdminUsersPage />
</TabsContent>
<TabsContent value="open-platform" className="mt-0">
<OpenPlatformPage embedded />
</TabsContent>
<TabsContent value="api-docs" className="mt-0">
<ApiDocsPage />
</TabsContent>

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
soulApp (soul-api) Go 项目一键部署到宝塔(正式环境)
soulApisoul-api 后端)Go 项目一键部署到宝塔(正式环境)
- 打包使用 .env.production 作为服务器 .env
- 本地交叉编译 Linux 二进制
- 上传到 /www/wwwroot/self/soul-api
@@ -322,7 +322,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
print(" [成功] 已解压到: %s" % project_path)
if not no_restart:
print("[4/4] 重启 soulApp 服务 ...")
print("[4/4] 重启 soulApi 服务 ...")
ok = False
if restart_method in ("auto", "btapi") and (cfg.get("bt_panel_url") and cfg.get("bt_api_key")):
ok = restart_via_bt_api(cfg)
@@ -342,7 +342,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
print(" [stderr] %s" % err[:200])
ok = "RESTART_OK" in out
if ok:
print(" [成功] soulApp 已通过 SSH 重启")
print(" [成功] soulApi 已通过 SSH 重启")
else:
print(" [警告] SSH 重启状态未知,请到宝塔 Go 项目里手动点击启动,或执行: cd %s && ./soul-api" % project_path)
else:
@@ -368,7 +368,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
def main():
parser = argparse.ArgumentParser(
description="soulApp (soul-api) Go 项目一键部署到宝塔",
description="soulApisoul-api 后端)Go 项目一键部署到宝塔",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--no-build", action="store_true", help="跳过本地编译(使用已有 soul-api 二进制)")
@@ -387,7 +387,7 @@ def main():
cfg = get_cfg()
print("=" * 60)
print(" soulApp 一键部署到宝塔")
print(" soulApisoul-api一键部署到宝塔")
print("=" * 60)
print(" 服务器: %s@%s:%s" % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT))
print(" 目标目录: %s" % cfg["project_path"])

View File

@@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
@@ -108,10 +109,52 @@ func invalidateReadExtrasCache() {
cache.Del(context.Background(), cache.KeyConfigReadExtras)
}
// syncMissingSuperIndividualsAsPersons 将当前有效超级个体VIP 口径与 vip-members 一致)中尚未绑定 persons.user_id 的用户,
// 通过 ensurePersonForUser 追加为可 @ 人物(昵称不合法或存客宝失败时跳过并打日志)。
func syncMissingSuperIndividualsAsPersons(db *gorm.DB) {
ids := activeSuperIndividualUserIDs(db)
if len(ids) == 0 {
return
}
var bound []string
if err := db.Model(&model.Person{}).
Where("user_id IS NOT NULL AND user_id != ?", "").
Distinct("user_id").
Pluck("user_id", &bound).Error; err != nil {
log.Printf("[persons] sync: list bound user_id failed: %v", err)
return
}
boundSet := make(map[string]struct{}, len(bound))
for _, b := range bound {
boundSet[strings.TrimSpace(b)] = struct{}{}
}
any := false
for _, uid := range ids {
uid = strings.TrimSpace(uid)
if uid == "" {
continue
}
if _, ok := boundSet[uid]; ok {
continue
}
if err := ensurePersonForUser(db, uid); err != nil {
log.Printf("[persons] sync super-individual userId=%s: %v", uid, err)
continue
}
boundSet[uid] = struct{}{}
any = true
}
if any {
invalidateReadExtrasCache()
}
}
// DBPersonList GET /api/db/persons 管理端-@提及人物列表
func DBPersonList(c *gin.Context) {
db := database.DB()
syncMissingSuperIndividualsAsPersons(db)
var rows []model.Person
if err := database.DB().Order("name ASC").Find(&rows).Error; err != nil {
if err := db.Order("name ASC").Find(&rows).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}

View File

@@ -151,6 +151,37 @@ func DBVipMembersList(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
}
// activeSuperIndividualUserIDs 与 DBVipMembersList / 小程序超级个体口径一致的有效用户 id无条数上限供 @ 列表自动补人)
func activeSuperIndividualUserIDs(db *gorm.DB) []string {
var users []model.User
err := db.Table("users").
Select("id").
Where("is_vip = 1 AND vip_expire_date > ?", time.Now()).
Order("id ASC").
Find(&users).Error
if err != nil || len(users) == 0 {
var userIDs []string
db.Model(&model.Order{}).Select("DISTINCT user_id").
Where("(status = ? OR status = ?) AND (product_type = ? OR product_type = ?)", "paid", "completed", "fullbook", "vip").
Pluck("user_id", &userIDs)
if len(userIDs) == 0 {
return nil
}
if err := db.Select("id").Where("id IN ?", userIDs).Find(&users).Error; err != nil {
return nil
}
}
out := make([]string, 0, len(users))
for _, u := range users {
id := strings.TrimSpace(u.ID)
if id != "" {
out = append(out, id)
}
}
return out
}
// DBVipMemberWebhookSet PUT /api/db/vip-members/webhook
// 按超级个体用户维度配置飞书群 webhookVOX 地址)。
func DBVipMemberWebhookSet(c *gin.Context) {

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
soulApp (soul-api) Go 项目一键部署到宝塔(正式环境)
soulApisoul-api 后端)Go 项目一键部署到宝塔(正式环境)
- 打包使用 .env.production 作为服务器 .env
- 本地交叉编译 Linux 二进制
- 上传到 /www/wwwroot/self/soul-api
@@ -336,7 +336,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
print(" [成功] 已解压到: %s" % project_path)
if not no_restart:
print("[4/4] 重启 soulApp 服务 ...")
print("[4/4] 重启 soulApi 服务 ...")
ok = False
if restart_method in ("auto", "btapi") and (cfg.get("bt_panel_url") and cfg.get("bt_api_key")):
ok = restart_via_bt_api(cfg)
@@ -371,7 +371,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
out = stdout.read().decode("utf-8", errors="replace").strip()
ok = "RESTART_OK" in out
if ok:
print(" [成功] soulApp 已通过 SSH 重启")
print(" [成功] soulApi 已通过 SSH 重启")
else:
print(" [警告] SSH 重启状态未知,请到宝塔 Go 项目里手动点击启动,或执行: cd %s && ./soul-api" % project_path)
else:
@@ -397,7 +397,7 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
def main():
parser = argparse.ArgumentParser(
description="soulApp (soul-api) Go 项目一键部署到宝塔",
description="soulApisoul-api 后端)Go 项目一键部署到宝塔",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--no-build", action="store_true", help="跳过本地编译(使用已有 soul-api 二进制)")
@@ -416,7 +416,7 @@ def main():
cfg = get_cfg()
print("=" * 60)
print(" soulApp 一键部署到宝塔")
print(" soulApisoul-api一键部署到宝塔")
print("=" * 60)
print(" 服务器: %s@%s:%s" % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT))
print(" 目标目录: %s" % cfg["project_path"])