神射手:Docker/website 编排、开发文档收敛、popover 与依赖更新
Made-with: Cursor
@@ -11,10 +11,11 @@ alwaysApply: true
|
||||
每次开始对话前,必须先读取以下文档了解项目上下文:
|
||||
|
||||
```
|
||||
1. 开发文档/00_项目核心文档.md → 数据库结构、AI规则、API规范
|
||||
2. 开发文档/01_开发进度文档.md → 当前进度、待办任务、落地规范
|
||||
3. 开发文档/02_提示词文档.md → 历史对话提示词、默认规则
|
||||
1. 开发文档/10、项目管理/项目核心文档.md → 数据库结构、AI规则、API规范
|
||||
2. 开发文档/10、项目管理/开发进度.md → 当前进度、每日进度、待办任务
|
||||
3. 开发文档/10、项目管理/提示词存档.md → 历史对话提示词、默认规则
|
||||
```
|
||||
(开发文档仅保留 1~10 共 10 个目录,入口见 10、项目管理/README.md)
|
||||
|
||||
## 二、项目记忆要点
|
||||
|
||||
@@ -44,22 +45,20 @@ alwaysApply: true
|
||||
|
||||
## 三、对话后更新规范
|
||||
|
||||
每次对话结束后,必须更新以下内容:
|
||||
每次对话结束后,必须更新以下内容(路径均在 开发文档/10、项目管理/):
|
||||
|
||||
1. **开发进度文档**
|
||||
- 标记完成的任务 ✅
|
||||
- 添加完成日期
|
||||
- 更新待办任务
|
||||
1. **开发进度.md**
|
||||
- 在顶部「每日开发进度」表增加一行(日期、当日完成、进行中)
|
||||
- 标记完成的任务 ✅,更新待办任务
|
||||
|
||||
2. **提示词文档**
|
||||
- 记录用户提示词
|
||||
- 记录执行结果
|
||||
- 记录时间节点
|
||||
2. **提示词存档.md**
|
||||
- 记录用户提示词、执行结果、时间节点
|
||||
|
||||
3. **项目核心文档**
|
||||
- 新发现的规则
|
||||
- 数据库结构变更
|
||||
- API接口更新
|
||||
3. **项目核心文档.md**(若有变更)
|
||||
- 新发现的规则、数据库结构变更、API 接口更新
|
||||
|
||||
4. **优化与迭代.md**(若有新优化项)
|
||||
- 已完成事项或待办优化
|
||||
|
||||
## 四、UI设计规范
|
||||
|
||||
|
||||
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
# 神射手 Docker 构建排除
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env*
|
||||
*.md
|
||||
开发文档
|
||||
.apm
|
||||
.cursor
|
||||
scripts
|
||||
*.log
|
||||
.DS_Store
|
||||
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# 神射手数据中台 - Docker 镜像(NAS/生产)
|
||||
# 多阶段:构建 Next.js standalone → 运行
|
||||
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 依赖:仅复制 package.json,在 Linux 下安装以得到正确的 @next/swc-linux-*(避免 darwin 专用包)
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN (pnpm run build 2>/dev/null || npm run build)
|
||||
|
||||
# 运行阶段:仅保留 standalone 输出
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3117
|
||||
EXPOSE 3117
|
||||
|
||||
# 从构建阶段复制 standalone
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# 无 root 运行
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs && \
|
||||
chown -R nextjs:nodejs /app
|
||||
USER nextjs
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
37
README_website.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# website 编排(玩值电竞 + 神射手)
|
||||
|
||||
- **玩值电竞**:http://localhost:3001
|
||||
- **神射手**:http://localhost:3117
|
||||
|
||||
## 首次使用(必做一次)
|
||||
|
||||
1. **先启动 Docker Desktop**
|
||||
从「应用程序」打开 Docker Desktop,等待菜单栏/托盘图标就绪(不再转圈)。
|
||||
若未安装:从 https://www.docker.com/products/docker-desktop 安装。
|
||||
|
||||
2. **在本目录执行**
|
||||
```bash
|
||||
./启动website.sh
|
||||
```
|
||||
脚本会自动:若 Docker 未运行则尝试打开 Docker Desktop,等待就绪后执行 `docker compose up -d --build`。
|
||||
|
||||
3. 浏览器打开 **http://localhost:3001** 即可访问玩值电竞。
|
||||
|
||||
## 之后每次
|
||||
|
||||
- Docker Desktop 已开:直接 `./启动website.sh` 或 `docker compose up -d --build`。
|
||||
- Docker 未开:先打开 Docker Desktop,就绪后再执行上述命令。
|
||||
|
||||
## 报错「Cannot connect to the Docker daemon」
|
||||
|
||||
表示 Docker 未运行。请先打开 Docker Desktop 并等待完全启动,再执行 `./启动website.sh`。
|
||||
|
||||
## 浏览器 localhost:3001 报 ERR_CONNECTION_REFUSED
|
||||
|
||||
可能原因:容器在跑但容器内进程已退出、或端口未正确映射。在本目录执行:
|
||||
|
||||
```bash
|
||||
./诊断并修复3001.sh
|
||||
```
|
||||
|
||||
脚本会:查看容器状态与日志 → 执行 `docker compose down` 再 `up -d --build` → 检查 3001 是否被监听。完成后在浏览器访问 http://localhost:3001。
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { getMongoClient, intelligentSearch, queryFullProfile, getDatabaseStats } from "@/lib/mongodb"
|
||||
import { getMongoClient, intelligentSearch, queryFullProfile, getDatabaseStats, unifiedResolveToPhones } from "@/lib/mongodb"
|
||||
|
||||
// 消息类型
|
||||
interface ChatMessage {
|
||||
@@ -14,49 +14,67 @@ interface ChatMessage {
|
||||
data?: any
|
||||
}
|
||||
|
||||
// 解析用户意图
|
||||
function parseIntent(message: string): { type: string; query?: string; params?: any } {
|
||||
const msg = message.trim().toLowerCase()
|
||||
|
||||
// 查询手机号
|
||||
const phoneMatch = message.match(/(?:查|查询|搜索|找)?[::\s]*(\+?86)?1[3-9]\d{9}/g)
|
||||
if (phoneMatch) {
|
||||
const phone = phoneMatch[0].replace(/[查询搜索找::\s]/g, '').replace(/^\+?86/, '')
|
||||
return { type: "query_phone", query: phone }
|
||||
}
|
||||
|
||||
// 查询 QQ
|
||||
const qqMatch = message.match(/(?:qq|QQ)[::\s]*(\d{5,11})|(\d{5,11})\s*(?:qq|QQ)/i)
|
||||
if (qqMatch) {
|
||||
return { type: "query_qq", query: qqMatch[1] || qqMatch[2] }
|
||||
}
|
||||
|
||||
// 系统状态
|
||||
if (msg.includes("状态") || msg.includes("统计") || msg.includes("总量")) {
|
||||
return { type: "system_status" }
|
||||
}
|
||||
|
||||
// RFM 分析
|
||||
if (msg.includes("rfm") || msg.includes("估值") || msg.includes("价值")) {
|
||||
return { type: "rfm_analysis" }
|
||||
}
|
||||
|
||||
// 高价值用户
|
||||
if (msg.includes("高价值") || msg.includes("top") || msg.includes("排行")) {
|
||||
const limitMatch = msg.match(/(\d+)/)
|
||||
return { type: "high_value_users", params: { limit: limitMatch ? parseInt(limitMatch[1]) : 10 } }
|
||||
}
|
||||
|
||||
// 帮助
|
||||
if (msg.includes("帮助") || msg.includes("help") || msg === "?") {
|
||||
return { type: "help" }
|
||||
}
|
||||
|
||||
// 通用搜索
|
||||
return { type: "search", query: message }
|
||||
// 系统/统计类意图(非单人/多人查询)
|
||||
function isSystemIntent(msg: string): boolean {
|
||||
const m = msg.trim().toLowerCase()
|
||||
return !m || m === '?' ||
|
||||
/状态|统计|总量/.test(m) ||
|
||||
/^rfm|估值|价值$/.test(m) ||
|
||||
/^高价值|top|排行/.test(m) ||
|
||||
/帮助|help/.test(m)
|
||||
}
|
||||
|
||||
// 格式化用户数据
|
||||
// 将输入拆成多段(支持一人或多人:手机、QQ、身份证、姓名/关键词)
|
||||
function parseSegments(message: string): { type: 'phone' | 'qq' | 'id_card' | 'keyword'; value: string }[] {
|
||||
const raw = message.trim()
|
||||
const parts = raw.split(/[,,、\n]+/).map(s => s.trim()).filter(Boolean)
|
||||
if (parts.length === 0) return [{ type: 'keyword', value: raw }]
|
||||
|
||||
const segments: { type: 'phone' | 'qq' | 'id_card' | 'keyword'; value: string }[] = []
|
||||
for (const p of parts) {
|
||||
const v = p.replace(/^[查查询找搜索::\s]+/i, '').trim()
|
||||
if (!v) continue
|
||||
// 手机号:11 位 1[3-9] 开头
|
||||
if (/^(\+?86)?1[3-9]\d{9}$/.test(v.replace(/\s/g, ''))) {
|
||||
segments.push({ type: 'phone', value: v.replace(/\D/g, '').replace(/^86/, '') })
|
||||
continue
|
||||
}
|
||||
// QQ:带 qq 前缀或 5~10 位数字(11 位且非 1 开头当 QQ)
|
||||
const qqMatch = v.match(/(?:qq|QQ)[::\s]*(\d{5,11})|(\d{5,10})\s*(?:qq|QQ)?|(\d{5,11})/i)
|
||||
if (qqMatch) {
|
||||
const qq = (qqMatch[1] || qqMatch[2] || qqMatch[3] || '').trim()
|
||||
if (qq && qq.length >= 5 && qq.length <= 11 && !/^1[3-9]\d{9}$/.test(qq)) {
|
||||
segments.push({ type: 'qq', value: qq })
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 身份证:18 位或 17 位 + X
|
||||
if (/^\d{17}[\dXx]$/.test(v.replace(/\s/g, ''))) {
|
||||
segments.push({ type: 'id_card', value: v.replace(/\s/g, '') })
|
||||
continue
|
||||
}
|
||||
// 姓名/关键词
|
||||
segments.push({ type: 'keyword', value: v })
|
||||
}
|
||||
return segments.length ? segments : [{ type: 'keyword', value: raw }]
|
||||
}
|
||||
|
||||
// 解析用户意图(兼容旧单条逻辑)
|
||||
function parseIntent(message: string): { type: string; query?: string; params?: any } {
|
||||
const msg = message.trim().toLowerCase()
|
||||
if (isSystemIntent(message)) {
|
||||
if (/状态|统计|总量/.test(msg)) return { type: "system_status" }
|
||||
if (/rfm|估值|价值/.test(msg)) return { type: "rfm_analysis" }
|
||||
if (/高价值|top|排行/.test(msg)) {
|
||||
const limitMatch = message.match(/(\d+)/)
|
||||
return { type: "high_value_users", params: { limit: limitMatch ? parseInt(limitMatch[1]) : 10 } }
|
||||
}
|
||||
if (/帮助|help|\?/.test(msg)) return { type: "help" }
|
||||
}
|
||||
return { type: "unified_search", query: message }
|
||||
}
|
||||
|
||||
// 格式化用户数据(简要)
|
||||
function formatUserData(user: any): string {
|
||||
if (!user) return "未找到用户信息"
|
||||
|
||||
@@ -78,6 +96,87 @@ function formatUserData(user: any): string {
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// 完整地址(省市区/地区统一)
|
||||
function formatAddress(fullProfile: { valuation?: any; qqPhone?: any }): string {
|
||||
const v = fullProfile.valuation
|
||||
const q = fullProfile.qqPhone
|
||||
const province = v?.province || q?.省份 || ''
|
||||
const city = v?.city || q?.地区 || ''
|
||||
const extra = (v?.district || v?.区县 || (v as any)?.区 || '')
|
||||
return [province, city, extra].filter(Boolean).join(' ') || '—'
|
||||
}
|
||||
|
||||
// 统一标签(用户估值 + 存客宝合并去重)
|
||||
function getAllTags(fullProfile: { valuation?: any; ckbAsset?: any }): string[] {
|
||||
const tags = new Set<string>()
|
||||
fullProfile.valuation?.unified_tags?.forEach((t: string) => tags.add(t))
|
||||
fullProfile.valuation?.tags?.forEach((t: string) => tags.add(t))
|
||||
fullProfile.ckbAsset?.tags?.forEach((t: string) => tags.add(t))
|
||||
return Array.from(tags)
|
||||
}
|
||||
|
||||
// 完整用户画像(含手机、QQ、地址、全部标签,跨库)
|
||||
function formatFullPortrait(fullProfile: { valuation?: any; qqPhone?: any; ckbAsset?: any }, index?: number): string {
|
||||
const { valuation, qqPhone, ckbAsset } = fullProfile
|
||||
const lines: string[] = []
|
||||
const title = index != null ? `━━━ 第 ${index + 1} 条 · 完整用户画像 ━━━` : '━━━ 完整用户画像 ━━━'
|
||||
lines.push(title + '\n')
|
||||
|
||||
if (valuation) {
|
||||
lines.push('【用户估值】')
|
||||
if (valuation.name) lines.push(`👤 姓名: ${valuation.name}`)
|
||||
if (valuation.phone_masked || valuation.phone) lines.push(`📱 手机: ${valuation.phone_masked || valuation.phone}`)
|
||||
if (valuation.user_evaluation_score != null) lines.push(`⭐ 估值分: ${valuation.user_evaluation_score}`)
|
||||
if (valuation.user_level) lines.push(`🏆 等级: ${valuation.user_level}`)
|
||||
lines.push(`📍 地址: ${formatAddress(fullProfile)}`)
|
||||
const tags = getAllTags(fullProfile)
|
||||
if (tags.length) lines.push(`🏷️ 统一标签: ${tags.join('、')}`)
|
||||
if (valuation.traffic_pool?.pool_name) lines.push(`📦 流量池: ${valuation.traffic_pool.pool_name}`)
|
||||
if (valuation.source_channels?.length) lines.push(`📂 数据来源: ${valuation.source_channels.join(', ')}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (qqPhone) {
|
||||
lines.push('【QQ/手机关联】')
|
||||
lines.push(`💬 QQ: ${qqPhone.qq}`)
|
||||
if (qqPhone.QQ号评分 != null) lines.push(` QQ评分: ${qqPhone.QQ号评分}`)
|
||||
if (qqPhone.手机号评分 != null) lines.push(` 手机评分: ${qqPhone.手机号评分}`)
|
||||
if (qqPhone.运营商) lines.push(`📶 运营商: ${qqPhone.运营商}`)
|
||||
if (qqPhone.省份 || qqPhone.地区) lines.push(`📍 地区: ${qqPhone.省份 || ''} ${qqPhone.地区 || ''}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (ckbAsset) {
|
||||
lines.push('【存客宝资产】')
|
||||
if (ckbAsset.nickname) lines.push(`昵称: ${ckbAsset.nickname}`)
|
||||
if (ckbAsset.total_assets != null) lines.push(`资产: ${ckbAsset.total_assets}`)
|
||||
if (ckbAsset.tags?.length) lines.push(`标签: ${ckbAsset.tags.join(', ')}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
return lines.join('\n').trim()
|
||||
}
|
||||
|
||||
// 根据完整画像生成 AI 分析文案(规则摘要)
|
||||
function generateAIAnalysis(fullProfile: { valuation?: any; qqPhone?: any; ckbAsset?: any }): string {
|
||||
const { valuation, qqPhone, ckbAsset } = fullProfile
|
||||
const parts: string[] = []
|
||||
|
||||
if (valuation) {
|
||||
const level = valuation.user_level || '未分级'
|
||||
const score = valuation.user_evaluation_score ?? 0
|
||||
const addr = formatAddress(fullProfile)
|
||||
parts.push(`该用户为 ${level} 级用户,估值分 ${score},地址:${addr}。`)
|
||||
const tags = getAllTags(fullProfile)
|
||||
if (tags.length) parts.push(`统一标签:${tags.join('、')}。`)
|
||||
if (valuation.traffic_pool?.pool_name) parts.push(`归属流量池「${valuation.traffic_pool.pool_name}」。`)
|
||||
}
|
||||
if (qqPhone) parts.push(`已关联 QQ,运营商 ${qqPhone.运营商 || '未知'}。`)
|
||||
if (ckbAsset) parts.push(`存客宝有资产记录,可做精细化运营。`)
|
||||
if (parts.length === 0) return '暂无足够数据生成分析。'
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
// 处理 AI 聊天
|
||||
async function processChat(message: string): Promise<ChatMessage> {
|
||||
const startTime = Date.now()
|
||||
@@ -85,59 +184,46 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
|
||||
try {
|
||||
switch (intent.type) {
|
||||
case "query_phone": {
|
||||
const result = await queryFullProfile(intent.query!)
|
||||
if (result.valuation || result.qqPhone) {
|
||||
const user = {
|
||||
...result.valuation,
|
||||
qq: result.qqPhone?.qq,
|
||||
carrier: result.qqPhone?.运营商
|
||||
}
|
||||
case "unified_search": {
|
||||
const segments = parseSegments(message)
|
||||
const phones = await unifiedResolveToPhones(segments, 10)
|
||||
if (phones.length === 0) {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `🎯 手机号 ${intent.query} 查询结果:\n\n${formatUserData(user)}\n\n⏱️ 查询耗时: ${Date.now() - startTime}ms`,
|
||||
data: result
|
||||
content: `🤔 未找到与「${message}」相关的用户\n\n💡 支持:手机号、QQ、身份证、姓名/城市/省份,可多条用逗号分隔,最多列出前 10 条完整画像。`
|
||||
}
|
||||
} else {
|
||||
}
|
||||
const profiles: { fullProfile: any; aiAnalysis: string }[] = []
|
||||
for (const phone of phones) {
|
||||
const fullProfile = await queryFullProfile(phone)
|
||||
if (fullProfile.valuation || fullProfile.qqPhone) {
|
||||
profiles.push({
|
||||
fullProfile,
|
||||
aiAnalysis: generateAIAnalysis(fullProfile)
|
||||
})
|
||||
}
|
||||
}
|
||||
if (profiles.length === 0) {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `❌ 未找到手机号 ${intent.query} 的相关信息\n\n💡 提示: 请检查手机号是否正确(11位数字)`
|
||||
content: `❌ 未解析到有效用户画像\n\n⏱️ 耗时: ${Date.now() - startTime}ms`
|
||||
}
|
||||
}
|
||||
const contentParts: string[] = [
|
||||
`🔍 共找到 ${profiles.length} 条用户,完整画像如下(前 10 条)\n`
|
||||
]
|
||||
profiles.forEach((p, i) => {
|
||||
contentParts.push(formatFullPortrait(p.fullProfile, i))
|
||||
contentParts.push(`\n🤖 AI 分析:${p.aiAnalysis}\n`)
|
||||
})
|
||||
contentParts.push(`\n⏱️ 查询耗时: ${Date.now() - startTime}ms`)
|
||||
return {
|
||||
role: "assistant",
|
||||
content: contentParts.join('\n'),
|
||||
data: { profiles, list: profiles }
|
||||
}
|
||||
}
|
||||
|
||||
case "query_qq": {
|
||||
const client = await getMongoClient()
|
||||
const qqDb = client.db("KR_腾讯")
|
||||
let qqDoc = await qqDb.collection("QQ+手机").findOne({ qq: intent.query })
|
||||
if (!qqDoc) {
|
||||
qqDoc = await qqDb.collection("QQ+手机").findOne({ qq: parseInt(intent.query!) })
|
||||
}
|
||||
|
||||
if (qqDoc) {
|
||||
const phone = qqDoc.phone || qqDoc.手机号
|
||||
let userInfo = `🎯 QQ ${intent.query} 查询结果:\n\n`
|
||||
userInfo += `💬 QQ: ${qqDoc.qq}\n`
|
||||
userInfo += `📱 手机: ${phone ? phone.toString().replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '未知'}\n`
|
||||
userInfo += `📊 QQ评分: ${qqDoc.QQ号评分 || 'N/A'}\n`
|
||||
userInfo += `📊 手机评分: ${qqDoc.手机号评分 || 'N/A'}\n`
|
||||
userInfo += `📍 地区: ${qqDoc.省份 || ''}${qqDoc.地区 || ''}\n`
|
||||
userInfo += `📶 运营商: ${qqDoc.运营商 || 'N/A'}\n`
|
||||
userInfo += `\n⏱️ 查询耗时: ${Date.now() - startTime}ms`
|
||||
|
||||
return {
|
||||
role: "assistant",
|
||||
content: userInfo,
|
||||
data: qqDoc
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `❌ 未找到 QQ ${intent.query} 的相关信息`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case "system_status": {
|
||||
const stats = await getDatabaseStats()
|
||||
return {
|
||||
@@ -206,39 +292,18 @@ async function processChat(message: string): Promise<ChatMessage> {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `🎯 神射手 AI 助手使用指南\n\n` +
|
||||
`📱 查询手机号:\n "查 13800138000" 或 "13800138000"\n\n` +
|
||||
`💬 查询 QQ:\n "28533368 qq" 或 "qq 28533368"\n\n` +
|
||||
`📊 系统状态:\n "系统状态" 或 "统计"\n\n` +
|
||||
`📈 RFM 分析:\n "RFM分析" 或 "用户估值"\n\n` +
|
||||
`🏆 高价值用户:\n "高价值用户 TOP10"\n\n` +
|
||||
`💡 数据覆盖: 20亿+用户,207GB数据`
|
||||
`📱 手机 / 💬 QQ / 🪪 身份证 / 👤 姓名:统一搜索,最多列出前 10 条完整画像\n\n` +
|
||||
`• 支持多条同时查:用逗号或换行分隔,如 "13800138000, 28533368 qq, 张三"\n` +
|
||||
`• 每条画像含:手机、QQ、地址、统一标签、流量池、存客宝、AI 分析\n\n` +
|
||||
`📊 系统状态 / 🏆 高价值 / 📈 RFM:输入 "系统状态"、"高价值用户 TOP10"、"RFM分析"\n\n` +
|
||||
`💡 数据覆盖: 20亿+用户,曼谷库内数据统一展示`
|
||||
}
|
||||
}
|
||||
|
||||
case "search":
|
||||
default: {
|
||||
// 尝试智能搜索
|
||||
const result = await intelligentSearch(intent.query || message, { limit: 5 })
|
||||
if (result.total > 0) {
|
||||
let content = `🔍 搜索结果 (共${result.total}条)\n\n`
|
||||
result.users.slice(0, 5).forEach((u, i) => {
|
||||
content += `${i + 1}. ${u.name || '未知'} - ${u.city || ''} - 估值: ${u.user_evaluation_score || 'N/A'}\n`
|
||||
})
|
||||
content += `\n⏱️ 查询耗时: ${Date.now() - startTime}ms`
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
data: result
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `🤔 我不太理解 "${message}"\n\n` +
|
||||
`💡 你可以尝试:\n` +
|
||||
`- 查询手机号: "查 13800138000"\n` +
|
||||
`- 查询QQ: "28533368 qq"\n` +
|
||||
`- 输入 "帮助" 查看更多功能`
|
||||
}
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `🤔 未识别指令\n\n输入 "帮助" 查看支持:手机、QQ、身份证、姓名,可多条件,前 10 条完整画像。`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,7 +364,7 @@ export async function GET() {
|
||||
return NextResponse.json({
|
||||
status: "online",
|
||||
model: "神射手 AI v1.0",
|
||||
capabilities: ["用户查询", "QQ查询", "RFM分析", "智能搜索"],
|
||||
capabilities: ["关键字搜索", "完整用户画像", "AI分析", "手机/QQ查询", "RFM分析"],
|
||||
database: {
|
||||
connected: stats.connected,
|
||||
totalUsers: stats.totalDocuments,
|
||||
|
||||
209
app/api/api-keys/billing/route.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// 调用日志接口
|
||||
interface CallLog {
|
||||
id: string
|
||||
keyId: string
|
||||
keyName: string
|
||||
endpoint: string
|
||||
method: string
|
||||
status: number
|
||||
credits: number
|
||||
responseTime: number
|
||||
timestamp: string
|
||||
ip: string
|
||||
requestFields?: string[]
|
||||
}
|
||||
|
||||
// 计费明细接口
|
||||
interface BillingDetail {
|
||||
category: string
|
||||
callCount: number
|
||||
avgPrice: number
|
||||
totalCredits: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
// 内存存储调用日志
|
||||
const callLogs: CallLog[] = [
|
||||
{ id: 'log_1', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/user', method: 'GET', status: 200, credits: 5, responseTime: 123, timestamp: '2026-01-31 14:32:15', ip: '123.45.67.89' },
|
||||
{ id: 'log_2', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/users/batch', method: 'POST', status: 200, credits: 40, responseTime: 856, timestamp: '2026-01-31 14:30:02', ip: '123.45.67.89' },
|
||||
{ id: 'log_3', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/ai/chat', method: 'POST', status: 200, credits: 5, responseTime: 2341, timestamp: '2026-01-31 14:28:45', ip: '98.76.54.32' },
|
||||
{ id: 'log_4', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/tags', method: 'GET', status: 200, credits: 0.5, responseTime: 45, timestamp: '2026-01-31 14:25:18', ip: '123.45.67.89' },
|
||||
{ id: 'log_5', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/user', method: 'GET', status: 403, credits: 0, responseTime: 12, timestamp: '2026-01-31 14:20:33', ip: '98.76.54.32' },
|
||||
{ id: 'log_6', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/ai/analyze', method: 'POST', status: 200, credits: 10, responseTime: 5623, timestamp: '2026-01-31 14:15:00', ip: '123.45.67.89' },
|
||||
{ id: 'log_7', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/packages/create', method: 'POST', status: 200, credits: 5, responseTime: 1234, timestamp: '2026-01-31 14:10:22', ip: '123.45.67.89' },
|
||||
{ id: 'log_8', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/ingest', method: 'POST', status: 429, credits: 0, responseTime: 8, timestamp: '2026-01-31 14:05:11', ip: '98.76.54.32' },
|
||||
]
|
||||
|
||||
// API分类映射
|
||||
const ENDPOINT_CATEGORIES: Record<string, string> = {
|
||||
'/api/shensheshou/user': '用户查询',
|
||||
'/api/shensheshou/users/batch': '用户查询',
|
||||
'/api/shensheshou/tags': '标签服务',
|
||||
'/api/shensheshou/tags/apply': '标签服务',
|
||||
'/api/shensheshou/ai/chat': 'AI服务',
|
||||
'/api/shensheshou/ai/analyze': 'AI服务',
|
||||
'/api/shensheshou/ai/tag': 'AI服务',
|
||||
'/api/shensheshou/sources': '数据服务',
|
||||
'/api/shensheshou/ingest': '数据服务',
|
||||
'/api/shensheshou/report/generate': '报告服务',
|
||||
'/api/shensheshou/packages': '流量包',
|
||||
'/api/shensheshou/packages/create': '流量包',
|
||||
'/api/shensheshou/packages/export': '流量包',
|
||||
}
|
||||
|
||||
// GET: 获取调用日志和计费明细
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action')
|
||||
const keyId = searchParams.get('keyId')
|
||||
const startDate = searchParams.get('startDate')
|
||||
const endDate = searchParams.get('endDate')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || '20')
|
||||
|
||||
// 筛选日志
|
||||
let filteredLogs = [...callLogs]
|
||||
|
||||
if (keyId) {
|
||||
filteredLogs = filteredLogs.filter(log => log.keyId === keyId)
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
filteredLogs = filteredLogs.filter(log => log.timestamp >= startDate)
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredLogs = filteredLogs.filter(log => log.timestamp <= endDate)
|
||||
}
|
||||
|
||||
// 获取计费明细
|
||||
if (action === 'billing') {
|
||||
const categoryStats: Record<string, { count: number; credits: number }> = {}
|
||||
let totalCredits = 0
|
||||
|
||||
filteredLogs.forEach(log => {
|
||||
const category = ENDPOINT_CATEGORIES[log.endpoint] || '其他'
|
||||
if (!categoryStats[category]) {
|
||||
categoryStats[category] = { count: 0, credits: 0 }
|
||||
}
|
||||
categoryStats[category].count++
|
||||
categoryStats[category].credits += log.credits
|
||||
totalCredits += log.credits
|
||||
})
|
||||
|
||||
const billingDetails: BillingDetail[] = Object.entries(categoryStats).map(([category, stats]) => ({
|
||||
category,
|
||||
callCount: stats.count,
|
||||
avgPrice: stats.count > 0 ? Number((stats.credits / stats.count).toFixed(2)) : 0,
|
||||
totalCredits: stats.credits,
|
||||
percentage: totalCredits > 0 ? Number(((stats.credits / totalCredits) * 100).toFixed(1)) : 0
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
details: billingDetails.sort((a, b) => b.totalCredits - a.totalCredits),
|
||||
totalCredits,
|
||||
totalCalls: filteredLogs.length,
|
||||
period: {
|
||||
start: startDate || filteredLogs[filteredLogs.length - 1]?.timestamp?.split(' ')[0],
|
||||
end: endDate || filteredLogs[0]?.timestamp?.split(' ')[0]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取统计概览
|
||||
if (action === 'stats') {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const todayLogs = filteredLogs.filter(log => log.timestamp.startsWith(today))
|
||||
const successLogs = filteredLogs.filter(log => log.status === 200)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
today: {
|
||||
calls: todayLogs.length,
|
||||
credits: todayLogs.reduce((sum, log) => sum + log.credits, 0),
|
||||
avgResponseTime: todayLogs.length > 0
|
||||
? Math.round(todayLogs.reduce((sum, log) => sum + log.responseTime, 0) / todayLogs.length)
|
||||
: 0
|
||||
},
|
||||
total: {
|
||||
calls: filteredLogs.length,
|
||||
credits: filteredLogs.reduce((sum, log) => sum + log.credits, 0),
|
||||
successRate: filteredLogs.length > 0
|
||||
? Number(((successLogs.length / filteredLogs.length) * 100).toFixed(1))
|
||||
: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分页返回日志
|
||||
const startIndex = (page - 1) * pageSize
|
||||
const paginatedLogs = filteredLogs.slice(startIndex, startIndex + pageSize)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: paginatedLogs,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total: filteredLogs.length,
|
||||
totalPages: Math.ceil(filteredLogs.length / pageSize)
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取计费信息失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '获取计费信息失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST: 记录API调用
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { keyId, keyName, endpoint, method, status, credits, responseTime, ip, requestFields } = body
|
||||
|
||||
const newLog: CallLog = {
|
||||
id: `log_${Date.now()}`,
|
||||
keyId,
|
||||
keyName,
|
||||
endpoint,
|
||||
method,
|
||||
status,
|
||||
credits: credits || 0,
|
||||
responseTime: responseTime || 0,
|
||||
timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
ip: ip || 'unknown',
|
||||
requestFields
|
||||
}
|
||||
|
||||
callLogs.unshift(newLog) // 添加到开头
|
||||
|
||||
// 保持日志数量在合理范围
|
||||
if (callLogs.length > 10000) {
|
||||
callLogs.splice(10000)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: newLog
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('记录调用日志失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '记录调用日志失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
387
app/api/api-keys/route.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// API密钥接口
|
||||
interface APIKey {
|
||||
id: string
|
||||
name: string
|
||||
key: string
|
||||
secret: string
|
||||
status: 'active' | 'disabled' | 'expired'
|
||||
createdAt: string
|
||||
expiresAt: string | null
|
||||
lastUsed: string | null
|
||||
permissions: FieldPermission[]
|
||||
rateLimit: {
|
||||
requestsPerDay: number
|
||||
requestsPerMonth: number
|
||||
}
|
||||
billing: {
|
||||
plan: 'free' | 'basic' | 'pro' | 'enterprise'
|
||||
usedCredits: number
|
||||
totalCredits: number
|
||||
}
|
||||
callStats: {
|
||||
today: number
|
||||
thisMonth: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
// 字段权限接口
|
||||
interface FieldPermission {
|
||||
fieldGroup: string
|
||||
fields: {
|
||||
name: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
price: number
|
||||
}[]
|
||||
}
|
||||
|
||||
// 计费套餐
|
||||
const BILLING_PLANS = {
|
||||
free: { credits: 1000, requestsPerDay: 100, requestsPerMonth: 3000 },
|
||||
basic: { credits: 10000, requestsPerDay: 1000, requestsPerMonth: 30000 },
|
||||
pro: { credits: 50000, requestsPerDay: 5000, requestsPerMonth: 150000 },
|
||||
enterprise: { credits: -1, requestsPerDay: -1, requestsPerMonth: -1 },
|
||||
}
|
||||
|
||||
// 默认字段权限
|
||||
const DEFAULT_FIELD_PERMISSIONS: FieldPermission[] = [
|
||||
{
|
||||
fieldGroup: '基础信息',
|
||||
fields: [
|
||||
{ name: 'phone', label: '手机号', enabled: true, price: 1 },
|
||||
{ name: 'qq', label: 'QQ号', enabled: true, price: 1 },
|
||||
{ name: 'wechat', label: '微信号', enabled: false, price: 2 },
|
||||
{ name: 'email', label: '邮箱', enabled: false, price: 1 },
|
||||
{ name: 'nickname', label: '昵称', enabled: true, price: 0.5 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '用户画像',
|
||||
fields: [
|
||||
{ name: 'rfm_score', label: 'RFM评分', enabled: true, price: 2 },
|
||||
{ name: 'user_level', label: '用户等级', enabled: true, price: 1 },
|
||||
{ name: 'value_score', label: '价值评分', enabled: false, price: 3 },
|
||||
{ name: 'activity_score', label: '活跃度评分', enabled: false, price: 2 },
|
||||
{ name: 'loyalty_score', label: '忠诚度评分', enabled: false, price: 2 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '标签数据',
|
||||
fields: [
|
||||
{ name: 'basic_tags', label: '基础标签', enabled: true, price: 1 },
|
||||
{ name: 'behavior_tags', label: '行为标签', enabled: false, price: 2 },
|
||||
{ name: 'preference_tags', label: '偏好标签', enabled: false, price: 2 },
|
||||
{ name: 'ai_tags', label: 'AI智能标签', enabled: false, price: 5 },
|
||||
{ name: 'custom_tags', label: '自定义标签', enabled: true, price: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '行为数据',
|
||||
fields: [
|
||||
{ name: 'last_active', label: '最后活跃时间', enabled: true, price: 0.5 },
|
||||
{ name: 'visit_count', label: '访问次数', enabled: false, price: 1 },
|
||||
{ name: 'purchase_history', label: '购买历史', enabled: false, price: 5 },
|
||||
{ name: 'interaction_log', label: '交互记录', enabled: false, price: 3 },
|
||||
{ name: 'channel_source', label: '渠道来源', enabled: true, price: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '扩展数据',
|
||||
fields: [
|
||||
{ name: 'social_bindings', label: '社交绑定', enabled: false, price: 3 },
|
||||
{ name: 'device_info', label: '设备信息', enabled: false, price: 2 },
|
||||
{ name: 'location_data', label: '位置数据', enabled: false, price: 4 },
|
||||
{ name: 'risk_assessment', label: '风险评估', enabled: false, price: 5 },
|
||||
{ name: 'ai_insights', label: 'AI洞察', enabled: false, price: 10 },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
// 内存存储(生产环境应使用MongoDB)
|
||||
let apiKeys: APIKey[] = [
|
||||
{
|
||||
id: 'key_1',
|
||||
name: '存客宝-生产环境',
|
||||
key: 'sk-archer-ckb-prod-a1b2c3d4e5f6',
|
||||
secret: 'sec-ckb-x9y8z7w6v5u4',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-15',
|
||||
expiresAt: null,
|
||||
lastUsed: '2026-01-31 14:32:15',
|
||||
permissions: JSON.parse(JSON.stringify(DEFAULT_FIELD_PERMISSIONS)),
|
||||
rateLimit: { requestsPerDay: 5000, requestsPerMonth: 150000 },
|
||||
billing: { plan: 'pro', usedCredits: 12580, totalCredits: 50000 },
|
||||
callStats: { today: 342, thisMonth: 8956, total: 45678 }
|
||||
},
|
||||
{
|
||||
id: 'key_2',
|
||||
name: '点了码-测试环境',
|
||||
key: 'sk-archer-dlm-test-g7h8i9j0k1l2',
|
||||
secret: 'sec-dlm-m3n4o5p6q7r8',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-20',
|
||||
expiresAt: '2026-04-20',
|
||||
lastUsed: '2026-01-30 09:15:42',
|
||||
permissions: JSON.parse(JSON.stringify(DEFAULT_FIELD_PERMISSIONS)).map((g: FieldPermission) => ({
|
||||
...g,
|
||||
fields: g.fields.map(f => ({ ...f, enabled: f.price <= 2 }))
|
||||
})),
|
||||
rateLimit: { requestsPerDay: 1000, requestsPerMonth: 30000 },
|
||||
billing: { plan: 'basic', usedCredits: 2340, totalCredits: 10000 },
|
||||
callStats: { today: 56, thisMonth: 1234, total: 5678 }
|
||||
},
|
||||
]
|
||||
|
||||
// 生成随机密钥
|
||||
function generateKey(prefix: string): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = prefix
|
||||
for (let i = 0; i < 20; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GET: 获取API密钥列表
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action')
|
||||
const keyId = searchParams.get('id')
|
||||
|
||||
// 验证单个密钥
|
||||
if (action === 'validate') {
|
||||
const apiKey = searchParams.get('key')
|
||||
const apiSecret = searchParams.get('secret')
|
||||
|
||||
const key = apiKeys.find(k => k.key === apiKey && k.secret === apiSecret)
|
||||
|
||||
if (!key) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '无效的API密钥'
|
||||
}, { status: 401 })
|
||||
}
|
||||
|
||||
if (key.status !== 'active') {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `密钥状态: ${key.status}`
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (key.expiresAt && new Date(key.expiresAt) < new Date()) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密钥已过期'
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
plan: key.billing.plan,
|
||||
permissions: key.permissions,
|
||||
rateLimit: key.rateLimit,
|
||||
creditsRemaining: key.billing.totalCredits === -1 ? -1 : key.billing.totalCredits - key.billing.usedCredits
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取单个密钥详情
|
||||
if (keyId) {
|
||||
const key = apiKeys.find(k => k.id === keyId)
|
||||
if (!key) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密钥不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: key
|
||||
})
|
||||
}
|
||||
|
||||
// 获取所有密钥列表
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys.map(k => ({
|
||||
...k,
|
||||
key: k.key.substring(0, 12) + '••••••••••••', // 脱敏显示
|
||||
secret: '••••••••••••••••'
|
||||
})),
|
||||
total: apiKeys.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取API密钥失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '获取API密钥失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST: 创建新API密钥
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { name, plan = 'basic', expiresAt } = body
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供密钥名称'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const planConfig = BILLING_PLANS[plan as keyof typeof BILLING_PLANS] || BILLING_PLANS.basic
|
||||
|
||||
const newKey: APIKey = {
|
||||
id: `key_${Date.now()}`,
|
||||
name,
|
||||
key: generateKey('sk-archer-'),
|
||||
secret: generateKey('sec-'),
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
expiresAt: expiresAt || null,
|
||||
lastUsed: null,
|
||||
permissions: JSON.parse(JSON.stringify(DEFAULT_FIELD_PERMISSIONS)),
|
||||
rateLimit: {
|
||||
requestsPerDay: planConfig.requestsPerDay,
|
||||
requestsPerMonth: planConfig.requestsPerMonth
|
||||
},
|
||||
billing: {
|
||||
plan: plan as 'free' | 'basic' | 'pro' | 'enterprise',
|
||||
usedCredits: 0,
|
||||
totalCredits: planConfig.credits
|
||||
},
|
||||
callStats: { today: 0, thisMonth: 0, total: 0 }
|
||||
}
|
||||
|
||||
apiKeys.push(newKey)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: newKey,
|
||||
message: '密钥创建成功'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建API密钥失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '创建API密钥失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PUT: 更新API密钥
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id, action, permissions, status } = body
|
||||
|
||||
const keyIndex = apiKeys.findIndex(k => k.id === id)
|
||||
if (keyIndex === -1) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密钥不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// 更新权限
|
||||
if (action === 'updatePermissions' && permissions) {
|
||||
apiKeys[keyIndex].permissions = permissions
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
message: '权限更新成功'
|
||||
})
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
if (action === 'toggleStatus') {
|
||||
apiKeys[keyIndex].status = apiKeys[keyIndex].status === 'active' ? 'disabled' : 'active'
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
message: `密钥已${apiKeys[keyIndex].status === 'active' ? '启用' : '禁用'}`
|
||||
})
|
||||
}
|
||||
|
||||
// 重新生成密钥
|
||||
if (action === 'regenerate') {
|
||||
apiKeys[keyIndex].key = generateKey('sk-archer-')
|
||||
apiKeys[keyIndex].secret = generateKey('sec-')
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
message: '密钥已重新生成'
|
||||
})
|
||||
}
|
||||
|
||||
// 常规更新
|
||||
if (status) {
|
||||
apiKeys[keyIndex].status = status
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: apiKeys[keyIndex],
|
||||
message: '更新成功'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新API密钥失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '更新API密钥失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: 删除API密钥
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const id = searchParams.get('id')
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供密钥ID'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const keyIndex = apiKeys.findIndex(k => k.id === id)
|
||||
if (keyIndex === -1) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密钥不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
const deletedKey = apiKeys.splice(keyIndex, 1)[0]
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { id: deletedKey.id, name: deletedKey.name },
|
||||
message: '密钥已删除'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('删除API密钥失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '删除API密钥失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
789
app/api/docs/route.ts
Normal file
@@ -0,0 +1,789 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// 神射手开放API文档 - OpenAPI 3.0 格式
|
||||
const OPENAPI_SPEC = {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: '神射手数据中台 API',
|
||||
description: `
|
||||
神射手是一个用户资产数字化平台,提供用户画像、AI标签、RFM评分等数据服务。
|
||||
|
||||
## 认证方式
|
||||
所有API请求需要在Header中携带API密钥:
|
||||
- **Authorization**: Bearer YOUR_API_KEY(必需)
|
||||
- **X-API-Secret**: YOUR_API_SECRET(可选,增强安全性)
|
||||
|
||||
## 基础信息
|
||||
- 基础URL: https://your-domain.com/api/shensheshou
|
||||
- 请求格式: JSON
|
||||
- 响应格式: JSON
|
||||
- 字符编码: UTF-8
|
||||
|
||||
## 计费说明
|
||||
- 每次API调用消耗积分(credits)
|
||||
- 不同接口消耗积分不同
|
||||
- 批量接口享受折扣
|
||||
|
||||
## 字段权限
|
||||
返回字段根据API密钥配置的字段权限决定,可在开放API管理中配置。
|
||||
`.trim(),
|
||||
version: '1.0.0',
|
||||
contact: {
|
||||
name: '神射手技术支持',
|
||||
email: 'support@shensheshou.com',
|
||||
},
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: '{protocol}://{host}/api/shensheshou',
|
||||
description: '神射手API服务器',
|
||||
variables: {
|
||||
protocol: { default: 'https', enum: ['https', 'http'] },
|
||||
host: { default: 'your-domain.com' },
|
||||
},
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{ name: 'user', description: '用户查询服务 - 查询用户画像和基本信息' },
|
||||
{ name: 'tag', description: '标签服务 - 获取、应用和管理用户标签' },
|
||||
{ name: 'ai', description: 'AI服务 - AI对话、智能分析和自动打标' },
|
||||
{ name: 'data', description: '数据服务 - 数据流入和同步' },
|
||||
{ name: 'package', description: '流量包 - 创建和导出用户流量包' },
|
||||
],
|
||||
paths: {
|
||||
'/': {
|
||||
get: {
|
||||
tags: ['user'],
|
||||
operationId: 'queryUser',
|
||||
summary: '用户画像查询',
|
||||
description: '根据手机号或QQ查询完整用户画像。返回字段根据API密钥权限决定。',
|
||||
parameters: [
|
||||
{ name: 'endpoint', in: 'query', required: true, schema: { type: 'string', enum: ['user'] }, description: '固定值: user' },
|
||||
{ name: 'phone', in: 'query', required: false, schema: { type: 'string', pattern: '^1[3-9]\\d{9}$' }, description: '11位手机号' },
|
||||
{ name: 'qq', in: 'query', required: false, schema: { type: 'string', pattern: '^[1-9]\\d{4,10}$' }, description: 'QQ号码(5-11位)' },
|
||||
{ name: 'fields', in: 'query', required: false, schema: { type: 'string' }, description: '指定返回字段(逗号分隔): phone,qq,rfm_score,user_level,tags,behavior,location' },
|
||||
],
|
||||
responses: {
|
||||
'200': {
|
||||
description: '查询成功',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/UserResponse' },
|
||||
example: {
|
||||
success: true,
|
||||
data: {
|
||||
phone: '138****8000',
|
||||
rfm_score: 85,
|
||||
user_level: 'A',
|
||||
tags: ['高价值', '活跃用户', '电商偏好'],
|
||||
last_active: '2026-01-30',
|
||||
},
|
||||
credits_used: 5,
|
||||
credits_remaining: 995,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'401': { $ref: '#/components/responses/Unauthorized' },
|
||||
'404': { $ref: '#/components/responses/NotFound' },
|
||||
},
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
post: {
|
||||
tags: ['user', 'data', 'ai'],
|
||||
operationId: 'postEndpoint',
|
||||
summary: '通用POST端点',
|
||||
description: '根据endpoint参数调用不同服务:users/batch(批量查询)、ingest(数据流入)、ai/chat(AI对话)、ai/tag(AI打标)等',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/PostRequest' },
|
||||
examples: {
|
||||
batch_query: {
|
||||
summary: '批量用户查询',
|
||||
value: {
|
||||
endpoint: 'users/batch',
|
||||
phones: ['13800138001', '13800138002'],
|
||||
fields: ['rfm_score', 'tags', 'user_level'],
|
||||
},
|
||||
},
|
||||
data_ingest: {
|
||||
summary: '数据流入',
|
||||
value: {
|
||||
endpoint: 'ingest',
|
||||
source: 'cunkebao',
|
||||
users: [
|
||||
{ phone: '13800138001', name: '张三', tags: ['高意向'] },
|
||||
{ phone: '13800138002', name: '李四' },
|
||||
],
|
||||
},
|
||||
},
|
||||
ai_chat: {
|
||||
summary: 'AI对话',
|
||||
value: {
|
||||
endpoint: 'ai/chat',
|
||||
message: '帮我查询13800138000的用户画像',
|
||||
},
|
||||
},
|
||||
ai_tag: {
|
||||
summary: 'AI智能打标',
|
||||
value: {
|
||||
endpoint: 'ai/tag',
|
||||
phones: ['13800138001', '13800138002'],
|
||||
strategy: 'rfm',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: '请求成功',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/GenericResponse' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'401': { $ref: '#/components/responses/Unauthorized' },
|
||||
'400': { $ref: '#/components/responses/BadRequest' },
|
||||
},
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
},
|
||||
'/tags': {
|
||||
get: {
|
||||
tags: ['tag'],
|
||||
operationId: 'listTags',
|
||||
summary: '获取标签列表',
|
||||
description: '获取系统中所有可用标签及其统计信息',
|
||||
parameters: [
|
||||
{ name: 'category', in: 'query', required: false, schema: { type: 'string', enum: ['value', 'behavior', 'preference', 'custom'] }, description: '标签分类' },
|
||||
],
|
||||
responses: {
|
||||
'200': {
|
||||
description: '成功',
|
||||
content: {
|
||||
'application/json': {
|
||||
example: {
|
||||
success: true,
|
||||
data: {
|
||||
tags: [
|
||||
{ id: 'tag_001', name: '高价值用户', category: 'value', count: 125000 },
|
||||
{ id: 'tag_002', name: '活跃用户', category: 'behavior', count: 450000 },
|
||||
],
|
||||
total: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
},
|
||||
'/packages': {
|
||||
get: {
|
||||
tags: ['package'],
|
||||
operationId: 'listPackages',
|
||||
summary: '获取流量包列表',
|
||||
description: '获取已创建的流量包列表',
|
||||
responses: {
|
||||
'200': {
|
||||
description: '成功',
|
||||
content: {
|
||||
'application/json': {
|
||||
example: {
|
||||
success: true,
|
||||
data: {
|
||||
packages: [
|
||||
{ id: 'pkg_001', name: '高价值用户包', count: 5000, created_at: '2026-01-30' },
|
||||
],
|
||||
total: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
post: {
|
||||
tags: ['package'],
|
||||
operationId: 'createPackage',
|
||||
summary: '创建流量包',
|
||||
description: '根据筛选条件创建用户流量包',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
example: {
|
||||
endpoint: 'packages/create',
|
||||
name: '高价值用户包',
|
||||
filters: {
|
||||
user_level: ['S', 'A'],
|
||||
tags: ['高价值'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: '创建成功',
|
||||
content: {
|
||||
'application/json': {
|
||||
example: {
|
||||
success: true,
|
||||
data: {
|
||||
package_id: 'pkg_002',
|
||||
name: '高价值用户包',
|
||||
count: 3500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ bearerAuth: [] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
description: 'API密钥,格式: sk-xxx',
|
||||
},
|
||||
apiSecret: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'X-API-Secret',
|
||||
description: 'API密钥Secret(可选,增强安全性)',
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
UserResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
data: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
phone: { type: 'string', description: '脱敏手机号' },
|
||||
qq: { type: 'string', description: 'QQ号' },
|
||||
rfm_score: { type: 'integer', minimum: 0, maximum: 100, description: 'RFM综合评分' },
|
||||
user_level: { type: 'string', enum: ['S', 'A', 'B', 'C', 'D'], description: '用户等级' },
|
||||
tags: { type: 'array', items: { type: 'string' }, description: '用户标签列表' },
|
||||
behavior: { type: 'object', description: '行为数据' },
|
||||
location: { type: 'object', description: '位置信息' },
|
||||
last_active: { type: 'string', format: 'date', description: '最后活跃时间' },
|
||||
},
|
||||
},
|
||||
credits_used: { type: 'integer', description: '本次消耗积分' },
|
||||
credits_remaining: { type: 'integer', description: '剩余积分' },
|
||||
},
|
||||
},
|
||||
PostRequest: {
|
||||
type: 'object',
|
||||
required: ['endpoint'],
|
||||
properties: {
|
||||
endpoint: { type: 'string', description: '端点名称' },
|
||||
},
|
||||
additionalProperties: true,
|
||||
},
|
||||
GenericResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
data: { type: 'object' },
|
||||
message: { type: 'string' },
|
||||
credits_used: { type: 'integer' },
|
||||
credits_remaining: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: false },
|
||||
error: { type: 'string' },
|
||||
code: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
Unauthorized: {
|
||||
description: '认证失败',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/Error' },
|
||||
example: { success: false, error: '无效的API密钥', code: 'INVALID_API_KEY' },
|
||||
},
|
||||
},
|
||||
},
|
||||
NotFound: {
|
||||
description: '资源不存在',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/Error' },
|
||||
example: { success: false, error: '用户不存在', code: 'USER_NOT_FOUND' },
|
||||
},
|
||||
},
|
||||
},
|
||||
BadRequest: {
|
||||
description: '请求参数错误',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/Error' },
|
||||
example: { success: false, error: '缺少必要参数', code: 'MISSING_PARAMS' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Markdown格式的API文档
|
||||
const MARKDOWN_DOC = `# 神射手数据中台 API 文档
|
||||
|
||||
> 版本: 1.0.0 | 更新时间: 2026-01-31
|
||||
|
||||
## 概述
|
||||
|
||||
神射手是一个**用户资产数字化平台**,提供用户画像、AI标签、RFM评分等数据服务。通过本API,第三方应用可以:
|
||||
|
||||
- 查询用户画像和标签
|
||||
- 批量导入用户数据并自动完善标签
|
||||
- 使用AI进行智能对话和分析
|
||||
- 创建和导出用户流量包
|
||||
|
||||
---
|
||||
|
||||
## 认证方式
|
||||
|
||||
所有API请求需要在Header中携带API密钥:
|
||||
|
||||
\`\`\`
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
X-API-Secret: YOUR_API_SECRET # 可选,增强安全性
|
||||
Content-Type: application/json
|
||||
\`\`\`
|
||||
|
||||
### 获取API密钥
|
||||
|
||||
1. 登录神射手管理后台
|
||||
2. 进入「数据市场 → 开放接口」
|
||||
3. 添加接入方并获取API Key和Secret
|
||||
|
||||
---
|
||||
|
||||
## 基础URL
|
||||
|
||||
\`\`\`
|
||||
https://your-domain.com/api/shensheshou
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 用户画像查询
|
||||
|
||||
查询单个用户的完整画像信息。
|
||||
|
||||
**请求**
|
||||
|
||||
\`\`\`http
|
||||
GET /api/shensheshou?endpoint=user&phone=13800138000
|
||||
\`\`\`
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|:----:|------|
|
||||
| endpoint | string | 是 | 固定值: user |
|
||||
| phone | string | 否 | 11位手机号(与qq二选一) |
|
||||
| qq | string | 否 | QQ号码(与phone二选一) |
|
||||
| fields | string | 否 | 指定返回字段,逗号分隔 |
|
||||
|
||||
**可用字段**: phone, qq, wechat, rfm_score, user_level, tags, behavior, location
|
||||
|
||||
**响应示例**
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"phone": "138****8000",
|
||||
"rfm_score": 85,
|
||||
"user_level": "A",
|
||||
"tags": ["高价值", "活跃用户", "电商偏好"],
|
||||
"last_active": "2026-01-30"
|
||||
},
|
||||
"credits_used": 5,
|
||||
"credits_remaining": 995
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### 2. 批量用户查询
|
||||
|
||||
批量查询多个用户画像,最多100个/次。
|
||||
|
||||
**请求**
|
||||
|
||||
\`\`\`http
|
||||
POST /api/shensheshou
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"endpoint": "users/batch",
|
||||
"phones": ["13800138001", "13800138002"],
|
||||
"fields": ["rfm_score", "tags", "user_level"]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**响应示例**
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"users": [
|
||||
{"phone": "138****8001", "rfm_score": 78, "tags": ["活跃"], "user_level": "B"},
|
||||
{"phone": "138****8002", "rfm_score": 92, "tags": ["高价值"], "user_level": "A"}
|
||||
],
|
||||
"found": 2,
|
||||
"not_found": []
|
||||
},
|
||||
"credits_used": 8
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### 3. 数据流入
|
||||
|
||||
将外部系统用户数据导入神射手,自动完善标签。
|
||||
|
||||
**请求**
|
||||
|
||||
\`\`\`http
|
||||
POST /api/shensheshou
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"endpoint": "ingest",
|
||||
"source": "cunkebao",
|
||||
"users": [
|
||||
{"phone": "13800138001", "name": "张三", "tags": ["高意向"]},
|
||||
{"phone": "13800138002", "name": "李四"}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**响应示例**
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 2,
|
||||
"processed": 2,
|
||||
"enriched": 2,
|
||||
"new_tags_added": 8
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### 4. AI对话
|
||||
|
||||
与神射手AI进行自然语言对话,支持用户查询、数据分析等。
|
||||
|
||||
**请求**
|
||||
|
||||
\`\`\`http
|
||||
POST /api/shensheshou
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"endpoint": "ai/chat",
|
||||
"message": "帮我查询13800138000的用户画像"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**响应示例**
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"reply": "该用户是A级高价值用户,RFM评分85分,标签包括:高价值、活跃用户、电商偏好。最近30天内有5次互动记录。",
|
||||
"user_data": {...}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### 5. AI智能打标
|
||||
|
||||
使用AI为用户批量打标签。
|
||||
|
||||
**请求**
|
||||
|
||||
\`\`\`http
|
||||
POST /api/shensheshou
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"endpoint": "ai/tag",
|
||||
"phones": ["13800138001", "13800138002"],
|
||||
"strategy": "rfm"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**策略选项**: rfm(RFM评分), behavior(行为分析), preference(偏好分析)
|
||||
|
||||
---
|
||||
|
||||
### 6. 获取标签列表
|
||||
|
||||
获取系统中所有可用标签。
|
||||
|
||||
**请求**
|
||||
|
||||
\`\`\`http
|
||||
GET /api/shensheshou?endpoint=tags&category=value
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### 7. 创建流量包
|
||||
|
||||
根据条件筛选用户并创建流量包。
|
||||
|
||||
**请求**
|
||||
|
||||
\`\`\`http
|
||||
POST /api/shensheshou
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"endpoint": "packages/create",
|
||||
"name": "高价值用户包",
|
||||
"filters": {
|
||||
"user_level": ["S", "A"],
|
||||
"tags": ["高价值"]
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| INVALID_API_KEY | API密钥无效或已过期 |
|
||||
| PERMISSION_DENIED | 无权限访问该接口 |
|
||||
| FIELD_NOT_ALLOWED | 无权限访问该字段 |
|
||||
| QUOTA_EXCEEDED | 调用配额已用尽 |
|
||||
| RATE_LIMITED | 请求频率超限 |
|
||||
| USER_NOT_FOUND | 用户不存在 |
|
||||
| MISSING_PARAMS | 缺少必要参数 |
|
||||
| INVALID_PARAMS | 参数格式错误 |
|
||||
|
||||
---
|
||||
|
||||
## 计费说明
|
||||
|
||||
| 接口 | 单价(积分/次) |
|
||||
|------|----------------|
|
||||
| 用户画像查询 | 1 |
|
||||
| 批量用户查询 | 0.8/人 |
|
||||
| 数据流入 | 0.5/人 |
|
||||
| AI对话 | 5 |
|
||||
| AI智能打标 | 2/人 |
|
||||
| 流量包创建 | 10 |
|
||||
| 流量包导出 | 0.1/人 |
|
||||
|
||||
---
|
||||
|
||||
## SDK示例
|
||||
|
||||
### Python
|
||||
|
||||
\`\`\`python
|
||||
import requests
|
||||
|
||||
API_KEY = "sk-archer-xxxxx"
|
||||
BASE_URL = "https://your-domain.com/api/shensheshou"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 查询用户画像
|
||||
response = requests.get(
|
||||
f"{BASE_URL}?endpoint=user&phone=13800138000",
|
||||
headers=headers
|
||||
)
|
||||
print(response.json())
|
||||
|
||||
# 批量查询
|
||||
response = requests.post(BASE_URL, headers=headers, json={
|
||||
"endpoint": "users/batch",
|
||||
"phones": ["13800138001", "13800138002"]
|
||||
})
|
||||
print(response.json())
|
||||
\`\`\`
|
||||
|
||||
### JavaScript
|
||||
|
||||
\`\`\`javascript
|
||||
const API_KEY = "sk-archer-xxxxx";
|
||||
const BASE_URL = "https://your-domain.com/api/shensheshou";
|
||||
|
||||
// 查询用户画像
|
||||
const response = await fetch(\`\${BASE_URL}?endpoint=user&phone=13800138000\`, {
|
||||
headers: {
|
||||
"Authorization": \`Bearer \${API_KEY}\`,
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
// 批量查询
|
||||
const batchResponse = await fetch(BASE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": \`Bearer \${API_KEY}\`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
endpoint: "users/batch",
|
||||
phones: ["13800138001", "13800138002"]
|
||||
})
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
### cURL
|
||||
|
||||
\`\`\`bash
|
||||
# 查询用户画像
|
||||
curl -X GET "https://your-domain.com/api/shensheshou?endpoint=user&phone=13800138000" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx"
|
||||
|
||||
# 批量查询
|
||||
curl -X POST "https://your-domain.com/api/shensheshou" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"endpoint":"users/batch","phones":["13800138001","13800138002"]}'
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## 联系支持
|
||||
|
||||
- 技术支持邮箱: support@shensheshou.com
|
||||
- 在线文档: https://docs.shensheshou.com
|
||||
|
||||
---
|
||||
|
||||
*文档版本 1.0.0 - 最后更新: 2026-01-31*
|
||||
`
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const format = searchParams.get('format') || 'json'
|
||||
|
||||
// 获取当前域名作为服务器URL
|
||||
const host = request.headers.get('host') || 'localhost:3000'
|
||||
const protocol = request.headers.get('x-forwarded-proto') || 'http'
|
||||
|
||||
// 更新服务器URL
|
||||
const spec = {
|
||||
...OPENAPI_SPEC,
|
||||
servers: [
|
||||
{
|
||||
url: `${protocol}://${host}/api/shensheshou`,
|
||||
description: '神射手API服务器',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
if (format === 'openapi' || format === 'json') {
|
||||
return NextResponse.json(spec, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (format === 'markdown' || format === 'md') {
|
||||
return new NextResponse(MARKDOWN_DOC, {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="shensheshou-api-docs.md"',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (format === 'download') {
|
||||
return NextResponse.json(spec, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': 'attachment; filename="shensheshou-openapi.json"',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 返回简化版文档信息
|
||||
return NextResponse.json({
|
||||
name: '神射手数据中台 API',
|
||||
version: '1.0.0',
|
||||
description: '用户资产数字化平台API,提供用户画像、AI标签、RFM评分等数据服务',
|
||||
docs: {
|
||||
openapi: `${protocol}://${host}/api/docs?format=openapi`,
|
||||
markdown: `${protocol}://${host}/api/docs?format=markdown`,
|
||||
download: `${protocol}://${host}/api/docs?format=download`,
|
||||
web: `${protocol}://${host}/data-market/api/docs`,
|
||||
},
|
||||
endpoints: {
|
||||
base: `${protocol}://${host}/api/shensheshou`,
|
||||
auth: 'Header: Authorization: Bearer YOUR_API_KEY',
|
||||
},
|
||||
quick_start: {
|
||||
step1: '获取API密钥:登录管理后台 → 数据市场 → 开放接口',
|
||||
step2: '阅读文档:访问上方 docs.web 链接',
|
||||
step3: '测试调用:使用curl或SDK调用API',
|
||||
},
|
||||
}, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 支持CORS
|
||||
export async function OPTIONS() {
|
||||
return new NextResponse(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
})
|
||||
}
|
||||
640
app/api/shensheshou/route.ts
Normal file
@@ -0,0 +1,640 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
interface FieldPermission {
|
||||
fieldGroup: string
|
||||
fields: {
|
||||
name: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
price: number
|
||||
}[]
|
||||
}
|
||||
|
||||
interface APIKeyInfo {
|
||||
id: string
|
||||
name: string
|
||||
plan: string
|
||||
permissions: FieldPermission[]
|
||||
rateLimit: {
|
||||
requestsPerDay: number
|
||||
requestsPerMonth: number
|
||||
}
|
||||
creditsRemaining: number
|
||||
}
|
||||
|
||||
interface UserProfile {
|
||||
phone?: string
|
||||
qq?: string
|
||||
wechat?: string
|
||||
email?: string
|
||||
nickname?: string
|
||||
rfm_score?: number
|
||||
user_level?: string
|
||||
value_score?: number
|
||||
activity_score?: number
|
||||
loyalty_score?: number
|
||||
basic_tags?: string[]
|
||||
behavior_tags?: string[]
|
||||
preference_tags?: string[]
|
||||
ai_tags?: string[]
|
||||
custom_tags?: string[]
|
||||
last_active?: string
|
||||
visit_count?: number
|
||||
purchase_history?: object[]
|
||||
interaction_log?: object[]
|
||||
channel_source?: string
|
||||
social_bindings?: object
|
||||
device_info?: object
|
||||
location_data?: object
|
||||
risk_assessment?: object
|
||||
ai_insights?: object
|
||||
}
|
||||
|
||||
// ==================== 模拟数据 ====================
|
||||
|
||||
// 模拟用户数据库
|
||||
const MOCK_USERS: Record<string, UserProfile> = {
|
||||
'13800138000': {
|
||||
phone: '13800138000',
|
||||
qq: '123456789',
|
||||
wechat: 'wx_zhangsan',
|
||||
email: 'zhangsan@example.com',
|
||||
nickname: '张三',
|
||||
rfm_score: 85,
|
||||
user_level: 'A',
|
||||
value_score: 92,
|
||||
activity_score: 78,
|
||||
loyalty_score: 88,
|
||||
basic_tags: ['高价值', '活跃用户'],
|
||||
behavior_tags: ['频繁购买', '喜欢促销'],
|
||||
preference_tags: ['数码产品', '时尚服饰'],
|
||||
ai_tags: ['潜在VIP', '消费升级'],
|
||||
custom_tags: ['重点客户'],
|
||||
last_active: '2026-01-30 15:30:00',
|
||||
visit_count: 156,
|
||||
purchase_history: [{ date: '2026-01-28', amount: 2580 }],
|
||||
interaction_log: [{ type: 'click', page: 'product', time: '2026-01-30' }],
|
||||
channel_source: '微信公众号',
|
||||
social_bindings: { wechat: true, qq: true, weibo: false },
|
||||
device_info: { os: 'iOS', browser: 'Safari' },
|
||||
location_data: { city: '上海', district: '浦东新区' },
|
||||
risk_assessment: { level: 'low', score: 15 },
|
||||
ai_insights: { nextAction: '推荐新品', churnRisk: 0.08 }
|
||||
},
|
||||
'13900139000': {
|
||||
phone: '13900139000',
|
||||
qq: '987654321',
|
||||
nickname: '李四',
|
||||
rfm_score: 65,
|
||||
user_level: 'B',
|
||||
value_score: 58,
|
||||
activity_score: 72,
|
||||
loyalty_score: 60,
|
||||
basic_tags: ['普通用户'],
|
||||
behavior_tags: ['偶尔购买'],
|
||||
last_active: '2026-01-25 10:15:00',
|
||||
visit_count: 45,
|
||||
channel_source: '搜索引擎'
|
||||
},
|
||||
'15800158000': {
|
||||
phone: '15800158000',
|
||||
nickname: '王五',
|
||||
rfm_score: 45,
|
||||
user_level: 'C',
|
||||
value_score: 38,
|
||||
activity_score: 52,
|
||||
loyalty_score: 40,
|
||||
basic_tags: ['新用户'],
|
||||
last_active: '2026-01-20 08:30:00',
|
||||
visit_count: 12,
|
||||
channel_source: '广告投放'
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟标签数据
|
||||
const MOCK_TAGS = [
|
||||
{ id: 'tag_1', name: '高价值', category: '价值标签', count: 12500 },
|
||||
{ id: 'tag_2', name: '活跃用户', category: '行为标签', count: 35800 },
|
||||
{ id: 'tag_3', name: '潜在VIP', category: 'AI标签', count: 8900 },
|
||||
{ id: 'tag_4', name: '流失预警', category: 'AI标签', count: 4500 },
|
||||
{ id: 'tag_5', name: '新用户', category: '基础标签', count: 28000 },
|
||||
{ id: 'tag_6', name: '频繁购买', category: '行为标签', count: 15600 },
|
||||
]
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
// 验证API密钥
|
||||
async function validateAPIKey(request: NextRequest): Promise<{ valid: boolean; keyInfo?: APIKeyInfo; error?: string }> {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const apiSecret = request.headers.get('X-API-Secret')
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return { valid: false, error: '缺少Authorization头' }
|
||||
}
|
||||
|
||||
const apiKey = authHeader.replace('Bearer ', '')
|
||||
|
||||
// 调用密钥验证API
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'
|
||||
const response = await fetch(`${baseUrl}/api/api-keys?action=validate&key=${apiKey}&secret=${apiSecret || ''}`)
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
return { valid: false, error: result.error }
|
||||
}
|
||||
|
||||
return { valid: true, keyInfo: result.data }
|
||||
} catch {
|
||||
// 备用验证(开发环境)
|
||||
if (apiKey.startsWith('sk-archer-')) {
|
||||
return {
|
||||
valid: true,
|
||||
keyInfo: {
|
||||
id: 'dev_key',
|
||||
name: '开发测试密钥',
|
||||
plan: 'pro',
|
||||
permissions: [],
|
||||
rateLimit: { requestsPerDay: -1, requestsPerMonth: -1 },
|
||||
creditsRemaining: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
return { valid: false, error: '密钥验证失败' }
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤用户数据(根据权限)
|
||||
function filterUserData(user: UserProfile, permissions: FieldPermission[], requestedFields?: string[]): Partial<UserProfile> {
|
||||
const allowedFields = new Set<string>()
|
||||
let totalPrice = 0
|
||||
|
||||
// 收集所有允许的字段
|
||||
permissions.forEach(group => {
|
||||
group.fields.forEach(field => {
|
||||
if (field.enabled) {
|
||||
allowedFields.add(field.name)
|
||||
if (!requestedFields || requestedFields.includes(field.name)) {
|
||||
totalPrice += field.price
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 如果指定了字段,只返回指定的字段
|
||||
const fieldsToReturn = requestedFields
|
||||
? requestedFields.filter(f => allowedFields.has(f))
|
||||
: Array.from(allowedFields)
|
||||
|
||||
const filteredUser: Partial<UserProfile> = {}
|
||||
|
||||
fieldsToReturn.forEach(field => {
|
||||
if (field in user) {
|
||||
(filteredUser as Record<string, unknown>)[field] = (user as Record<string, unknown>)[field]
|
||||
}
|
||||
})
|
||||
|
||||
return filteredUser
|
||||
}
|
||||
|
||||
// 计算信用点消耗
|
||||
function calculateCredits(permissions: FieldPermission[], requestedFields?: string[]): number {
|
||||
let total = 0
|
||||
|
||||
permissions.forEach(group => {
|
||||
group.fields.forEach(field => {
|
||||
if (field.enabled && (!requestedFields || requestedFields.includes(field.name))) {
|
||||
total += field.price
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// 记录API调用
|
||||
async function logAPICall(keyId: string, keyName: string, endpoint: string, method: string, status: number, credits: number, responseTime: number, ip: string) {
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'
|
||||
await fetch(`${baseUrl}/api/api-keys/billing`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ keyId, keyName, endpoint, method, status, credits, responseTime, ip })
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('记录API调用失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== API路由 ====================
|
||||
|
||||
// GET: 查询用户/标签等
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const endpoint = searchParams.get('endpoint') || 'user'
|
||||
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
||||
|
||||
// 验证API密钥
|
||||
const { valid, keyInfo, error } = await validateAPIKey(request)
|
||||
if (!valid || !keyInfo) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error || '认证失败'
|
||||
}, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
// 用户查询
|
||||
if (endpoint === 'user') {
|
||||
const phone = searchParams.get('phone')
|
||||
const qq = searchParams.get('qq')
|
||||
const fields = searchParams.get('fields')?.split(',')
|
||||
|
||||
if (!phone && !qq) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供phone或qq参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = phone
|
||||
? MOCK_USERS[phone]
|
||||
: Object.values(MOCK_USERS).find(u => u.qq === qq)
|
||||
|
||||
if (!user) {
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/user', 'GET', 404, 0, Date.now() - startTime, ip)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// 过滤数据
|
||||
const filteredUser = filterUserData(user, keyInfo.permissions, fields)
|
||||
const creditsUsed = calculateCredits(keyInfo.permissions, fields)
|
||||
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/user', 'GET', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: filteredUser,
|
||||
credits_used: creditsUsed,
|
||||
credits_remaining: keyInfo.creditsRemaining === -1 ? '无限' : keyInfo.creditsRemaining - creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// 标签列表
|
||||
if (endpoint === 'tags') {
|
||||
const category = searchParams.get('category')
|
||||
let tags = MOCK_TAGS
|
||||
|
||||
if (category) {
|
||||
tags = tags.filter(t => t.category === category)
|
||||
}
|
||||
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/tags', 'GET', 200, 0.5, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: tags,
|
||||
total: tags.length,
|
||||
credits_used: 0.5
|
||||
})
|
||||
}
|
||||
|
||||
// 数据源列表
|
||||
if (endpoint === 'sources') {
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/sources', 'GET', 200, 0.5, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: [
|
||||
{ id: 'KR_腾讯', name: '腾讯数据', status: 'connected', records: 700000000 },
|
||||
{ id: 'KR_微博', name: '微博数据', status: 'connected', records: 140000000 },
|
||||
{ id: 'KR_京东', name: '京东数据', status: 'connected', records: 50000000 },
|
||||
{ id: 'KR_存客宝', name: '存客宝数据', status: 'connected', records: 1200000 },
|
||||
],
|
||||
credits_used: 0.5
|
||||
})
|
||||
}
|
||||
|
||||
// 流量包列表
|
||||
if (endpoint === 'packages') {
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/packages', 'GET', 200, 0.5, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: [
|
||||
{ id: 'pkg_1', name: '高价值用户包', count: 12500, createdAt: '2026-01-28' },
|
||||
{ id: 'pkg_2', name: '活跃用户包', count: 35800, createdAt: '2026-01-25' },
|
||||
{ id: 'pkg_3', name: '流失预警包', count: 4500, createdAt: '2026-01-20' },
|
||||
],
|
||||
credits_used: 0.5
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '未知的endpoint'
|
||||
}, { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('API请求处理失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '服务器内部错误'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST: 批量查询/AI服务/数据导入等
|
||||
export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
||||
|
||||
// 验证API密钥
|
||||
const { valid, keyInfo, error } = await validateAPIKey(request)
|
||||
if (!valid || !keyInfo) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error || '认证失败'
|
||||
}, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { endpoint } = body
|
||||
|
||||
// 批量用户查询
|
||||
if (endpoint === 'users/batch') {
|
||||
const { phones = [], qqs = [], fields } = body
|
||||
|
||||
if (phones.length === 0 && qqs.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供phones或qqs数组'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
if (phones.length > 100 || qqs.length > 100) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '单次查询最多100个用户'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const results: Partial<UserProfile>[] = []
|
||||
|
||||
phones.forEach((phone: string) => {
|
||||
const user = MOCK_USERS[phone]
|
||||
if (user) {
|
||||
results.push(filterUserData(user, keyInfo.permissions, fields))
|
||||
}
|
||||
})
|
||||
|
||||
qqs.forEach((qq: string) => {
|
||||
const user = Object.values(MOCK_USERS).find(u => u.qq === qq)
|
||||
if (user) {
|
||||
results.push(filterUserData(user, keyInfo.permissions, fields))
|
||||
}
|
||||
})
|
||||
|
||||
const creditsUsed = results.length * 0.8 // 批量折扣
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/users/batch', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: results,
|
||||
total: results.length,
|
||||
credits_used: creditsUsed,
|
||||
credits_remaining: keyInfo.creditsRemaining === -1 ? '无限' : keyInfo.creditsRemaining - creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// AI对话
|
||||
if (endpoint === 'ai/chat') {
|
||||
const { message } = body
|
||||
|
||||
if (!message) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供message参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 模拟AI响应
|
||||
const creditsUsed = 5
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/ai/chat', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
response: {
|
||||
content: `根据您的查询"${message}",系统分析结果如下:目前数据库中共有高价值用户12,500位,其中本月新增1,234位。建议重点关注RFM评分高于80的用户群体。`,
|
||||
data: {
|
||||
totalUsers: 12500,
|
||||
newUsersThisMonth: 1234,
|
||||
avgRFM: 72
|
||||
},
|
||||
suggestions: [
|
||||
'可以进一步筛选活跃度高于80的用户',
|
||||
'建议导出为流量包进行精准营销'
|
||||
]
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// AI分析
|
||||
if (endpoint === 'ai/analyze') {
|
||||
const { type } = body
|
||||
|
||||
const creditsUsed = 10
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/ai/analyze', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
type,
|
||||
summary: `${type}分析完成`,
|
||||
insights: [
|
||||
{ metric: 'RFM平均分', value: 72, trend: '+5%' },
|
||||
{ metric: '活跃用户占比', value: '45%', trend: '+2%' },
|
||||
{ metric: '高价值用户', value: 12500, trend: '+8%' }
|
||||
],
|
||||
recommendations: [
|
||||
'建议对RFM分数65-75区间的用户进行激活营销',
|
||||
'高价值用户流失风险较低,可减少维护投入'
|
||||
]
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// AI打标
|
||||
if (endpoint === 'ai/tag') {
|
||||
const { user_ids = [] } = body
|
||||
|
||||
const creditsUsed = user_ids.length * 0.5
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/ai/tag', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
processed: user_ids.length,
|
||||
tagsApplied: user_ids.length * 3,
|
||||
summary: `已为${user_ids.length}个用户完成AI智能打标`
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// 应用标签
|
||||
if (endpoint === 'tags/apply') {
|
||||
const { user_id, tags } = body
|
||||
|
||||
if (!user_id || !tags || !Array.isArray(tags)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供user_id和tags数组'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const creditsUsed = 2
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/tags/apply', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
user_id,
|
||||
applied_tags: tags,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// 数据导入
|
||||
if (endpoint === 'ingest') {
|
||||
const { source, data: ingestData, auto_tag } = body
|
||||
|
||||
if (!source || !ingestData || !Array.isArray(ingestData)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供source和data数组'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const creditsUsed = ingestData.length * 0.1 + (auto_tag ? ingestData.length * 0.3 : 0)
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/ingest', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
source,
|
||||
imported: ingestData.length,
|
||||
auto_tagged: auto_tag ? ingestData.length : 0,
|
||||
task_id: `task_${Date.now()}`
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// 创建流量包
|
||||
if (endpoint === 'packages/create') {
|
||||
const { name, criteria } = body
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供流量包名称'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const creditsUsed = 5
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/packages/create', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: `pkg_${Date.now()}`,
|
||||
name,
|
||||
criteria,
|
||||
count: Math.floor(Math.random() * 10000) + 1000,
|
||||
status: 'created',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// 导出流量包
|
||||
if (endpoint === 'packages/export') {
|
||||
const { package_id, target } = body
|
||||
|
||||
if (!package_id || !target) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供package_id和target'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const creditsUsed = 10
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/packages/export', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
package_id,
|
||||
target,
|
||||
status: 'exporting',
|
||||
task_id: `export_${Date.now()}`,
|
||||
estimatedTime: '2分钟'
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
// 生成报告
|
||||
if (endpoint === 'report/generate') {
|
||||
const { template } = body
|
||||
|
||||
if (!template) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '请提供报告模板'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const creditsUsed = 15
|
||||
await logAPICall(keyInfo.id, keyInfo.name, '/api/shensheshou/report/generate', 'POST', 200, creditsUsed, Date.now() - startTime, ip)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
template,
|
||||
report_id: `report_${Date.now()}`,
|
||||
status: 'generating',
|
||||
estimatedTime: '5分钟',
|
||||
downloadUrl: null
|
||||
},
|
||||
credits_used: creditsUsed
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '未知的endpoint'
|
||||
}, { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('API请求处理失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '服务器内部错误'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
|
||||
888
app/data-market/api/docs/page.tsx
Normal file
@@ -0,0 +1,888 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Copy,
|
||||
CheckCircle2,
|
||||
Search,
|
||||
Tags,
|
||||
Brain,
|
||||
Database,
|
||||
FileText,
|
||||
Package,
|
||||
ArrowLeft,
|
||||
Code,
|
||||
Zap,
|
||||
Key,
|
||||
DollarSign,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Play,
|
||||
Terminal,
|
||||
BookOpen,
|
||||
Server,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Link2,
|
||||
Share2,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
// API端点接口
|
||||
interface APIEndpoint {
|
||||
id: string
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
path: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
auth: boolean
|
||||
price: number
|
||||
params?: { name: string; type: string; required: boolean; desc: string }[]
|
||||
response?: string
|
||||
example?: string
|
||||
}
|
||||
|
||||
// API分类
|
||||
const API_CATEGORIES = [
|
||||
{ id: 'query', name: '用户查询', icon: Search, description: '查询用户画像和基本信息' },
|
||||
{ id: 'tag', name: '标签服务', icon: Tags, description: '获取和应用用户标签' },
|
||||
{ id: 'ai', name: 'AI服务', icon: Brain, description: 'AI对话、分析和智能打标' },
|
||||
{ id: 'data', name: '数据服务', icon: Database, description: '数据源管理和数据导入' },
|
||||
{ id: 'report', name: '报告服务', icon: FileText, description: '生成数据分析报告' },
|
||||
{ id: 'package', name: '流量包', icon: Package, description: '创建和导出流量包' },
|
||||
]
|
||||
|
||||
// 预定义API端点
|
||||
const API_ENDPOINTS: APIEndpoint[] = [
|
||||
// 用户查询
|
||||
{
|
||||
id: 'api_1',
|
||||
method: 'GET',
|
||||
path: '/api/shensheshou/user',
|
||||
name: '用户画像查询',
|
||||
description: '根据手机号或QQ查询完整用户画像,返回字段根据API密钥权限决定',
|
||||
category: 'query',
|
||||
auth: true,
|
||||
price: 1,
|
||||
params: [
|
||||
{ name: 'phone', type: 'string', required: false, desc: '11位手机号' },
|
||||
{ name: 'qq', type: 'string', required: false, desc: 'QQ号码' },
|
||||
{ name: 'fields', type: 'string', required: false, desc: '指定返回字段,逗号分隔' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"phone": "138****8000",
|
||||
"rfm_score": 85,
|
||||
"user_level": "A",
|
||||
"tags": ["高价值", "活跃用户"],
|
||||
"last_active": "2026-01-30"
|
||||
},
|
||||
"credits_used": 5,
|
||||
"credits_remaining": 995
|
||||
}`,
|
||||
example: `curl -X GET "https://api.shensheshou.com/api/shensheshou?endpoint=user&phone=13800138000&fields=rfm_score,tags" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx"`,
|
||||
},
|
||||
{
|
||||
id: 'api_2',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/users/batch',
|
||||
name: '批量用户查询',
|
||||
description: '批量查询多个用户的画像信息,最多支持100个用户/次,享受批量折扣',
|
||||
category: 'query',
|
||||
auth: true,
|
||||
price: 0.8,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: users/batch' },
|
||||
{ name: 'phones', type: 'array', required: false, desc: '手机号数组,最多100个' },
|
||||
{ name: 'qqs', type: 'array', required: false, desc: 'QQ号数组,最多100个' },
|
||||
{ name: 'fields', type: 'array', required: false, desc: '指定返回字段数组' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "phone": "138****8000", "rfm_score": 85, "user_level": "A" },
|
||||
{ "phone": "139****9000", "rfm_score": 72, "user_level": "B" }
|
||||
],
|
||||
"total": 2,
|
||||
"credits_used": 8,
|
||||
"credits_remaining": 992
|
||||
}`,
|
||||
example: `curl -X POST "https://api.shensheshou.com/api/shensheshou" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"endpoint": "users/batch",
|
||||
"phones": ["13800138000", "13900139000"],
|
||||
"fields": ["rfm_score", "user_level", "tags"]
|
||||
}'`,
|
||||
},
|
||||
// 标签服务
|
||||
{
|
||||
id: 'api_3',
|
||||
method: 'GET',
|
||||
path: '/api/shensheshou/tags',
|
||||
name: '标签列表',
|
||||
description: '获取系统中所有可用标签及其分类和使用统计',
|
||||
category: 'tag',
|
||||
auth: true,
|
||||
price: 0.5,
|
||||
params: [
|
||||
{ name: 'category', type: 'string', required: false, desc: '标签分类筛选' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "tag_1", "name": "高价值", "category": "价值标签", "count": 12500 },
|
||||
{ "id": "tag_2", "name": "活跃用户", "category": "行为标签", "count": 35800 }
|
||||
],
|
||||
"total": 45,
|
||||
"credits_used": 0.5
|
||||
}`,
|
||||
example: `curl -X GET "https://api.shensheshou.com/api/shensheshou?endpoint=tags&category=价值标签" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx"`,
|
||||
},
|
||||
{
|
||||
id: 'api_4',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/tags/apply',
|
||||
name: '应用标签',
|
||||
description: '为指定用户批量应用标签',
|
||||
category: 'tag',
|
||||
auth: true,
|
||||
price: 2,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: tags/apply' },
|
||||
{ name: 'user_id', type: 'string', required: true, desc: '用户ID或手机号' },
|
||||
{ name: 'tags', type: 'array', required: true, desc: '要应用的标签ID数组' },
|
||||
{ name: 'overwrite', type: 'boolean', required: false, desc: '是否覆盖现有标签,默认false' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user_id": "13800138000",
|
||||
"applied_tags": ["tag_1", "tag_2"],
|
||||
"timestamp": "2026-01-31T14:30:00Z"
|
||||
},
|
||||
"credits_used": 2
|
||||
}`,
|
||||
example: `curl -X POST "https://api.shensheshou.com/api/shensheshou" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"endpoint": "tags/apply",
|
||||
"user_id": "13800138000",
|
||||
"tags": ["tag_1", "tag_2"],
|
||||
"overwrite": false
|
||||
}'`,
|
||||
},
|
||||
// AI服务
|
||||
{
|
||||
id: 'api_5',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/ai/chat',
|
||||
name: 'AI智能对话',
|
||||
description: '与神射手AI进行对话,支持自然语言查询和数据分析',
|
||||
category: 'ai',
|
||||
auth: true,
|
||||
price: 5,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: ai/chat' },
|
||||
{ name: 'message', type: 'string', required: true, desc: '对话内容' },
|
||||
{ name: 'context', type: 'object', required: false, desc: '上下文信息' },
|
||||
{ name: 'model', type: 'string', required: false, desc: 'AI模型: qwen/deepseek' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"response": {
|
||||
"content": "根据查询,共有 1,234 位高价值用户...",
|
||||
"data": { "count": 1234, "avg_rfm": 82 },
|
||||
"suggestions": ["可以进一步筛选活跃度", "建议导出为流量包"]
|
||||
},
|
||||
"credits_used": 5
|
||||
}`,
|
||||
example: `curl -X POST "https://api.shensheshou.com/api/shensheshou" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"endpoint": "ai/chat",
|
||||
"message": "帮我分析RFM评分大于80的用户群体",
|
||||
"model": "qwen"
|
||||
}'`,
|
||||
},
|
||||
{
|
||||
id: 'api_6',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/ai/analyze',
|
||||
name: 'AI数据分析',
|
||||
description: 'AI自动分析用户群体特征并生成洞察报告',
|
||||
category: 'ai',
|
||||
auth: true,
|
||||
price: 10,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: ai/analyze' },
|
||||
{ name: 'type', type: 'string', required: true, desc: '分析类型: rfm/behavior/preference/churn' },
|
||||
{ name: 'filters', type: 'object', required: false, desc: '用户筛选条件' },
|
||||
{ name: 'depth', type: 'string', required: false, desc: '分析深度: quick/standard/deep' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"type": "rfm",
|
||||
"summary": "RFM分析完成",
|
||||
"insights": [
|
||||
{ "metric": "RFM平均分", "value": 72, "trend": "+5%" },
|
||||
{ "metric": "高价值用户", "value": 12500, "trend": "+8%" }
|
||||
],
|
||||
"recommendations": [
|
||||
"建议对RFM分数65-75区间的用户进行激活营销"
|
||||
]
|
||||
},
|
||||
"credits_used": 10
|
||||
}`,
|
||||
example: `curl -X POST "https://api.shensheshou.com/api/shensheshou" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"endpoint": "ai/analyze",
|
||||
"type": "rfm",
|
||||
"filters": { "user_level": ["A", "B"] },
|
||||
"depth": "standard"
|
||||
}'`,
|
||||
},
|
||||
{
|
||||
id: 'api_7',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/ai/tag',
|
||||
name: 'AI智能打标',
|
||||
description: 'AI自动分析用户数据并智能打标签',
|
||||
category: 'ai',
|
||||
auth: true,
|
||||
price: 8,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: ai/tag' },
|
||||
{ name: 'user_ids', type: 'array', required: true, desc: '用户ID数组' },
|
||||
{ name: 'tag_types', type: 'array', required: false, desc: '指定标签类型' },
|
||||
{ name: 'model', type: 'string', required: false, desc: 'AI模型选择' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"processed": 50,
|
||||
"tagsApplied": 150,
|
||||
"summary": "已为50个用户完成AI智能打标"
|
||||
},
|
||||
"credits_used": 25
|
||||
}`,
|
||||
},
|
||||
// 数据服务
|
||||
{
|
||||
id: 'api_8',
|
||||
method: 'GET',
|
||||
path: '/api/shensheshou/sources',
|
||||
name: '数据源列表',
|
||||
description: '获取所有已接入的数据源及其连接状态和数据量',
|
||||
category: 'data',
|
||||
auth: true,
|
||||
price: 0.5,
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "KR_腾讯", "name": "腾讯数据", "status": "connected", "records": 700000000 },
|
||||
{ "id": "KR_微博", "name": "微博数据", "status": "connected", "records": 140000000 }
|
||||
],
|
||||
"credits_used": 0.5
|
||||
}`,
|
||||
example: `curl -X GET "https://api.shensheshou.com/api/shensheshou?endpoint=sources" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx"`,
|
||||
},
|
||||
{
|
||||
id: 'api_9',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/ingest',
|
||||
name: '数据导入',
|
||||
description: '导入外部数据到神射手平台,自动触发AI标签引擎处理',
|
||||
category: 'data',
|
||||
auth: true,
|
||||
price: 3,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: ingest' },
|
||||
{ name: 'source', type: 'string', required: true, desc: '数据源标识,如: cunkebao' },
|
||||
{ name: 'data', type: 'array', required: true, desc: '用户数据数组' },
|
||||
{ name: 'auto_tag', type: 'boolean', required: false, desc: '是否自动打标,默认true' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"source": "cunkebao",
|
||||
"imported": 100,
|
||||
"auto_tagged": 100,
|
||||
"task_id": "task_1706698200000"
|
||||
},
|
||||
"credits_used": 40
|
||||
}`,
|
||||
example: `curl -X POST "https://api.shensheshou.com/api/shensheshou" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"endpoint": "ingest",
|
||||
"source": "cunkebao",
|
||||
"data": [
|
||||
{ "phone": "13800138000", "name": "张三", "source": "微信" },
|
||||
{ "phone": "13900139000", "name": "李四", "source": "抖音" }
|
||||
],
|
||||
"auto_tag": true
|
||||
}'`,
|
||||
},
|
||||
// 报告服务
|
||||
{
|
||||
id: 'api_10',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/report/generate',
|
||||
name: '生成分析报告',
|
||||
description: 'AI自动生成数据分析报告,支持多种模板和格式',
|
||||
category: 'report',
|
||||
auth: true,
|
||||
price: 15,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: report/generate' },
|
||||
{ name: 'template', type: 'string', required: true, desc: '报告模板: user_insight/rfm_analysis/trend_report' },
|
||||
{ name: 'date_range', type: 'object', required: false, desc: '日期范围 {start, end}' },
|
||||
{ name: 'format', type: 'string', required: false, desc: '输出格式: pdf/html/json' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"template": "rfm_analysis",
|
||||
"report_id": "report_1706698200000",
|
||||
"status": "generating",
|
||||
"estimatedTime": "5分钟",
|
||||
"downloadUrl": null
|
||||
},
|
||||
"credits_used": 15
|
||||
}`,
|
||||
},
|
||||
// 流量包
|
||||
{
|
||||
id: 'api_11',
|
||||
method: 'GET',
|
||||
path: '/api/shensheshou/packages',
|
||||
name: '流量包列表',
|
||||
description: '获取所有已创建的流量包',
|
||||
category: 'package',
|
||||
auth: true,
|
||||
price: 0.5,
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "pkg_1", "name": "高价值用户包", "count": 12500, "createdAt": "2026-01-28" },
|
||||
{ "id": "pkg_2", "name": "活跃用户包", "count": 35800, "createdAt": "2026-01-25" }
|
||||
],
|
||||
"credits_used": 0.5
|
||||
}`,
|
||||
example: `curl -X GET "https://api.shensheshou.com/api/shensheshou?endpoint=packages" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx"`,
|
||||
},
|
||||
{
|
||||
id: 'api_12',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/packages/create',
|
||||
name: '创建流量包',
|
||||
description: '根据筛选条件创建用户流量包',
|
||||
category: 'package',
|
||||
auth: true,
|
||||
price: 5,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: packages/create' },
|
||||
{ name: 'name', type: 'string', required: true, desc: '流量包名称' },
|
||||
{ name: 'criteria', type: 'object', required: true, desc: '筛选条件' },
|
||||
{ name: 'export_fields', type: 'array', required: false, desc: '导出字段' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "pkg_1706698200000",
|
||||
"name": "高价值活跃用户",
|
||||
"criteria": { "rfm_score": { "$gte": 80 } },
|
||||
"count": 8500,
|
||||
"status": "created",
|
||||
"createdAt": "2026-01-31T14:30:00Z"
|
||||
},
|
||||
"credits_used": 5
|
||||
}`,
|
||||
example: `curl -X POST "https://api.shensheshou.com/api/shensheshou" \\
|
||||
-H "Authorization: Bearer sk-archer-xxxxx" \\
|
||||
-H "X-API-Secret: sec-xxxxx" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"endpoint": "packages/create",
|
||||
"name": "高价值活跃用户",
|
||||
"criteria": {
|
||||
"rfm_score": { "$gte": 80 },
|
||||
"user_level": ["A", "B"]
|
||||
},
|
||||
"export_fields": ["phone", "rfm_score", "tags"]
|
||||
}'`,
|
||||
},
|
||||
{
|
||||
id: 'api_13',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/packages/export',
|
||||
name: '导出流量包',
|
||||
description: '导出流量包数据到指定目标(邮箱、飞书、Webhook)',
|
||||
category: 'package',
|
||||
auth: true,
|
||||
price: 10,
|
||||
params: [
|
||||
{ name: 'endpoint', type: 'string', required: true, desc: '固定值: packages/export' },
|
||||
{ name: 'package_id', type: 'string', required: true, desc: '流量包ID' },
|
||||
{ name: 'target', type: 'string', required: true, desc: '导出目标: email/feishu/webhook' },
|
||||
{ name: 'config', type: 'object', required: false, desc: '导出配置(邮箱地址、Webhook URL等)' },
|
||||
],
|
||||
response: `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"package_id": "pkg_1706698200000",
|
||||
"target": "email",
|
||||
"status": "exporting",
|
||||
"task_id": "export_1706698200000",
|
||||
"estimatedTime": "2分钟"
|
||||
},
|
||||
"credits_used": 10
|
||||
}`,
|
||||
},
|
||||
]
|
||||
|
||||
export default function APIDocsPage() {
|
||||
const [activeCategory, setActiveCategory] = useState('all')
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState('')
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [expandedEndpoints, setExpandedEndpoints] = useState<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
const host = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
setApiBaseUrl(host)
|
||||
}, [])
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopiedId(id)
|
||||
setTimeout(() => setCopiedId(null), 2000)
|
||||
}
|
||||
|
||||
const getMethodColor = (method: string) => {
|
||||
switch (method) {
|
||||
case 'GET': return 'bg-green-100 text-green-700 border-green-200'
|
||||
case 'POST': return 'bg-blue-100 text-blue-700 border-blue-200'
|
||||
case 'PUT': return 'bg-yellow-100 text-yellow-700 border-yellow-200'
|
||||
case 'DELETE': return 'bg-red-100 text-red-700 border-red-200'
|
||||
default: return 'bg-gray-100 text-gray-700 border-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEndpoint = (id: string) => {
|
||||
const newSet = new Set(expandedEndpoints)
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id)
|
||||
} else {
|
||||
newSet.add(id)
|
||||
}
|
||||
setExpandedEndpoints(newSet)
|
||||
}
|
||||
|
||||
const filteredEndpoints = API_ENDPOINTS.filter(e => {
|
||||
const matchCategory = activeCategory === 'all' || e.category === activeCategory
|
||||
const matchSearch = !searchQuery ||
|
||||
e.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
e.path.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
e.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
return matchCategory && matchSearch
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/data-market/api">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
返回
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">API文档</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">神射手开放API完整文档 · 支持AI直接对接</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>v1.0</Badge>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(`${apiBaseUrl}/api/docs?format=openapi`, 'api-link')}
|
||||
>
|
||||
{copiedId === 'api-link' ? (
|
||||
<CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
|
||||
) : (
|
||||
<Link2 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
复制API链接
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`${apiBaseUrl}/api/docs?format=download`, '_blank')}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
下载OpenAPI
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`${apiBaseUrl}/api/docs?format=markdown`, '_blank')}
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
下载Markdown
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI对接提示卡片 */}
|
||||
<Card className="border-0 shadow-sm bg-gradient-to-r from-cyan-50 to-blue-50 border-l-4 border-l-cyan-500">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-cyan-100">
|
||||
<Brain className="h-5 w-5 text-cyan-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">AI直接对接</h3>
|
||||
<p className="text-sm text-gray-600 mb-2">将以下链接提供给AI(如ChatGPT、Claude),即可直接理解并调用神射手API</p>
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-white/80 font-mono text-sm">
|
||||
<code className="text-cyan-700 flex-1 truncate">{apiBaseUrl}/api/docs?format=openapi</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 shrink-0"
|
||||
onClick={() => copyToClipboard(`${apiBaseUrl}/api/docs?format=openapi`, 'openapi-link')}
|
||||
>
|
||||
{copiedId === 'openapi-link' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-xs">
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-white/80">
|
||||
<Badge variant="outline" className="text-xs">OpenAPI 3.0</Badge>
|
||||
<span className="text-gray-500">标准格式</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-white/80">
|
||||
<Badge variant="outline" className="text-xs">Markdown</Badge>
|
||||
<span className="text-gray-500">可读文档</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 快速入门 */}
|
||||
<Card className="border-0 shadow-sm bg-gradient-to-r from-purple-50 to-blue-50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 rounded-lg bg-purple-100">
|
||||
<BookOpen className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">快速入门</h3>
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div className="p-3 rounded-lg bg-white/80">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-5 h-5 rounded-full bg-purple-500 text-white text-xs flex items-center justify-center">1</span>
|
||||
<span className="font-medium text-sm">获取密钥</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">在密钥管理页面创建API密钥</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-white/80">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-5 h-5 rounded-full bg-purple-500 text-white text-xs flex items-center justify-center">2</span>
|
||||
<span className="font-medium text-sm">配置权限</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置可访问的数据字段</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-white/80">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-5 h-5 rounded-full bg-purple-500 text-white text-xs flex items-center justify-center">3</span>
|
||||
<span className="font-medium text-sm">开始调用</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">使用密钥调用API接口</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-gray-900">
|
||||
<p className="text-xs text-gray-400 mb-2">基础请求示例</p>
|
||||
<pre className="text-green-400 text-sm overflow-x-auto">
|
||||
{`curl -X GET "${apiBaseUrl}/api/shensheshou?endpoint=user&phone=13800138000" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-H "X-API-Secret: YOUR_API_SECRET"`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 搜索和分类 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索API..."
|
||||
className="pl-10"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 overflow-x-auto">
|
||||
<Button
|
||||
variant={activeCategory === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setActiveCategory('all')}
|
||||
>
|
||||
全部 ({API_ENDPOINTS.length})
|
||||
</Button>
|
||||
{API_CATEGORIES.map(cat => {
|
||||
const count = API_ENDPOINTS.filter(e => e.category === cat.id).length
|
||||
const Icon = cat.icon
|
||||
return (
|
||||
<Button
|
||||
key={cat.id}
|
||||
variant={activeCategory === cat.id ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
>
|
||||
<Icon className="h-4 w-4 mr-1" />
|
||||
{cat.name} ({count})
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API端点列表 */}
|
||||
<div className="space-y-4">
|
||||
{filteredEndpoints.map(endpoint => (
|
||||
<Card key={endpoint.id} className="border-0 shadow-sm bg-white/80 overflow-hidden">
|
||||
<div
|
||||
className="p-4 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
onClick={() => toggleEndpoint(endpoint.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge className={`${getMethodColor(endpoint.method)} font-mono border`}>
|
||||
{endpoint.method}
|
||||
</Badge>
|
||||
<code className="text-sm font-mono text-gray-700">{endpoint.path}</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
copyToClipboard(`${apiBaseUrl}${endpoint.path}`, endpoint.id)
|
||||
}}
|
||||
>
|
||||
{copiedId === endpoint.id ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<DollarSign className="h-3 w-3 mr-1" />
|
||||
{endpoint.price} 信用点
|
||||
</Badge>
|
||||
{endpoint.auth && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Key className="h-3 w-3 mr-1" />
|
||||
需认证
|
||||
</Badge>
|
||||
)}
|
||||
{expandedEndpoints.has(endpoint.id) ? (
|
||||
<ChevronDown className="h-4 w-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<h3 className="font-semibold text-gray-900">{endpoint.name}</h3>
|
||||
<p className="text-sm text-gray-500">{endpoint.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 展开详情 */}
|
||||
{expandedEndpoints.has(endpoint.id) && (
|
||||
<div className="border-t p-4 bg-gray-50 space-y-4">
|
||||
{/* 参数 */}
|
||||
{endpoint.params && endpoint.params.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">请求参数</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-500 border-b">
|
||||
<th className="pb-2 pr-4">参数名</th>
|
||||
<th className="pb-2 pr-4">类型</th>
|
||||
<th className="pb-2 pr-4">必填</th>
|
||||
<th className="pb-2">说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{endpoint.params.map((param, i) => (
|
||||
<tr key={i} className="border-b last:border-0">
|
||||
<td className="py-2 pr-4">
|
||||
<code className="bg-gray-100 px-2 py-0.5 rounded text-purple-600">{param.name}</code>
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<Badge variant="outline" className="text-xs">{param.type}</Badge>
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
{param.required ? (
|
||||
<Badge className="bg-red-100 text-red-700 text-xs">必填</Badge>
|
||||
) : (
|
||||
<span className="text-gray-400">可选</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 text-gray-600">{param.desc}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 调用示例 */}
|
||||
{endpoint.example && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-medium text-gray-700">调用示例</h4>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(endpoint.example!, endpoint.id + '_example')}
|
||||
>
|
||||
{copiedId === endpoint.id + '_example' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
<span className="ml-1">复制</span>
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="bg-gray-900 text-green-400 p-4 rounded-lg text-sm overflow-x-auto">
|
||||
{endpoint.example}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 响应示例 */}
|
||||
{endpoint.response && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-medium text-gray-700">响应示例</h4>
|
||||
<Badge className="bg-green-100 text-green-700">200 OK</Badge>
|
||||
</div>
|
||||
<pre className="bg-gray-100 p-4 rounded-lg text-sm font-mono text-gray-700 overflow-x-auto">
|
||||
{endpoint.response}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 错误码说明 */}
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">错误码说明</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-500 border-b">
|
||||
<th className="pb-2 pr-4">HTTP状态码</th>
|
||||
<th className="pb-2 pr-4">错误类型</th>
|
||||
<th className="pb-2">说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4"><Badge className="bg-green-100 text-green-700">200</Badge></td>
|
||||
<td className="py-2 pr-4">成功</td>
|
||||
<td className="py-2 text-gray-600">请求成功处理</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4"><Badge className="bg-yellow-100 text-yellow-700">400</Badge></td>
|
||||
<td className="py-2 pr-4">请求错误</td>
|
||||
<td className="py-2 text-gray-600">参数缺失或格式错误</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4"><Badge className="bg-red-100 text-red-700">401</Badge></td>
|
||||
<td className="py-2 pr-4">认证失败</td>
|
||||
<td className="py-2 text-gray-600">API密钥无效或缺失</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4"><Badge className="bg-orange-100 text-orange-700">403</Badge></td>
|
||||
<td className="py-2 pr-4">权限不足</td>
|
||||
<td className="py-2 text-gray-600">无权访问该资源或字段</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4"><Badge className="bg-gray-100 text-gray-700">404</Badge></td>
|
||||
<td className="py-2 pr-4">未找到</td>
|
||||
<td className="py-2 text-gray-600">请求的资源不存在</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="py-2 pr-4"><Badge className="bg-purple-100 text-purple-700">429</Badge></td>
|
||||
<td className="py-2 pr-4">限流</td>
|
||||
<td className="py-2 text-gray-600">超出调用频率限制或信用点不足</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 pr-4"><Badge className="bg-red-100 text-red-700">500</Badge></td>
|
||||
<td className="py-2 pr-4">服务器错误</td>
|
||||
<td className="py-2 text-gray-600">服务器内部错误,请稍后重试</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
772
app/data-market/api/keys/page.tsx
Normal file
@@ -0,0 +1,772 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import {
|
||||
Copy,
|
||||
CheckCircle2,
|
||||
Key,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Trash2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Settings,
|
||||
Shield,
|
||||
Lock,
|
||||
Unlock,
|
||||
ArrowLeft,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Activity,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
interface FieldPermission {
|
||||
fieldGroup: string
|
||||
fields: {
|
||||
name: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
price: number
|
||||
}[]
|
||||
}
|
||||
|
||||
interface APIKey {
|
||||
id: string
|
||||
name: string
|
||||
key: string
|
||||
secret: string
|
||||
status: 'active' | 'disabled' | 'expired'
|
||||
createdAt: string
|
||||
expiresAt: string | null
|
||||
lastUsed: string | null
|
||||
permissions: FieldPermission[]
|
||||
rateLimit: {
|
||||
requestsPerDay: number
|
||||
requestsPerMonth: number
|
||||
}
|
||||
billing: {
|
||||
plan: 'free' | 'basic' | 'pro' | 'enterprise'
|
||||
usedCredits: number
|
||||
totalCredits: number
|
||||
}
|
||||
callStats: {
|
||||
today: number
|
||||
thisMonth: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
// 计费套餐
|
||||
const BILLING_PLANS = [
|
||||
{ id: 'free', name: '免费版', price: 0, credits: 1000, requestsPerDay: 100, requestsPerMonth: 3000 },
|
||||
{ id: 'basic', name: '基础版', price: 99, credits: 10000, requestsPerDay: 1000, requestsPerMonth: 30000 },
|
||||
{ id: 'pro', name: '专业版', price: 299, credits: 50000, requestsPerDay: 5000, requestsPerMonth: 150000 },
|
||||
{ id: 'enterprise', name: '企业版', price: 999, credits: -1, requestsPerDay: -1, requestsPerMonth: -1 },
|
||||
]
|
||||
|
||||
// 字段分组及权限
|
||||
const FIELD_GROUPS: FieldPermission[] = [
|
||||
{
|
||||
fieldGroup: '基础信息',
|
||||
fields: [
|
||||
{ name: 'phone', label: '手机号', enabled: true, price: 1 },
|
||||
{ name: 'qq', label: 'QQ号', enabled: true, price: 1 },
|
||||
{ name: 'wechat', label: '微信号', enabled: false, price: 2 },
|
||||
{ name: 'email', label: '邮箱', enabled: false, price: 1 },
|
||||
{ name: 'nickname', label: '昵称', enabled: true, price: 0.5 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '用户画像',
|
||||
fields: [
|
||||
{ name: 'rfm_score', label: 'RFM评分', enabled: true, price: 2 },
|
||||
{ name: 'user_level', label: '用户等级', enabled: true, price: 1 },
|
||||
{ name: 'value_score', label: '价值评分', enabled: false, price: 3 },
|
||||
{ name: 'activity_score', label: '活跃度评分', enabled: false, price: 2 },
|
||||
{ name: 'loyalty_score', label: '忠诚度评分', enabled: false, price: 2 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '标签数据',
|
||||
fields: [
|
||||
{ name: 'basic_tags', label: '基础标签', enabled: true, price: 1 },
|
||||
{ name: 'behavior_tags', label: '行为标签', enabled: false, price: 2 },
|
||||
{ name: 'preference_tags', label: '偏好标签', enabled: false, price: 2 },
|
||||
{ name: 'ai_tags', label: 'AI智能标签', enabled: false, price: 5 },
|
||||
{ name: 'custom_tags', label: '自定义标签', enabled: true, price: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '行为数据',
|
||||
fields: [
|
||||
{ name: 'last_active', label: '最后活跃时间', enabled: true, price: 0.5 },
|
||||
{ name: 'visit_count', label: '访问次数', enabled: false, price: 1 },
|
||||
{ name: 'purchase_history', label: '购买历史', enabled: false, price: 5 },
|
||||
{ name: 'interaction_log', label: '交互记录', enabled: false, price: 3 },
|
||||
{ name: 'channel_source', label: '渠道来源', enabled: true, price: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
fieldGroup: '扩展数据',
|
||||
fields: [
|
||||
{ name: 'social_bindings', label: '社交绑定', enabled: false, price: 3 },
|
||||
{ name: 'device_info', label: '设备信息', enabled: false, price: 2 },
|
||||
{ name: 'location_data', label: '位置数据', enabled: false, price: 4 },
|
||||
{ name: 'risk_assessment', label: '风险评估', enabled: false, price: 5 },
|
||||
{ name: 'ai_insights', label: 'AI洞察', enabled: false, price: 10 },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
// 模拟API密钥数据
|
||||
const MOCK_API_KEYS: APIKey[] = [
|
||||
{
|
||||
id: 'key_1',
|
||||
name: '存客宝-生产环境',
|
||||
key: 'sk-archer-ckb-prod-a1b2c3d4e5f6',
|
||||
secret: 'sec-ckb-x9y8z7w6v5u4',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-15',
|
||||
expiresAt: null,
|
||||
lastUsed: '2026-01-31 14:32:15',
|
||||
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)),
|
||||
rateLimit: { requestsPerDay: 5000, requestsPerMonth: 150000 },
|
||||
billing: { plan: 'pro', usedCredits: 12580, totalCredits: 50000 },
|
||||
callStats: { today: 342, thisMonth: 8956, total: 45678 }
|
||||
},
|
||||
{
|
||||
id: 'key_2',
|
||||
name: '点了码-测试环境',
|
||||
key: 'sk-archer-dlm-test-g7h8i9j0k1l2',
|
||||
secret: 'sec-dlm-m3n4o5p6q7r8',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-20',
|
||||
expiresAt: '2026-04-20',
|
||||
lastUsed: '2026-01-30 09:15:42',
|
||||
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)).map((g: FieldPermission) => ({
|
||||
...g,
|
||||
fields: g.fields.map(f => ({ ...f, enabled: f.price <= 2 }))
|
||||
})),
|
||||
rateLimit: { requestsPerDay: 1000, requestsPerMonth: 30000 },
|
||||
billing: { plan: 'basic', usedCredits: 2340, totalCredits: 10000 },
|
||||
callStats: { today: 56, thisMonth: 1234, total: 5678 }
|
||||
},
|
||||
{
|
||||
id: 'key_3',
|
||||
name: '内部测试密钥',
|
||||
key: 'sk-archer-internal-s3t4u5v6w7x8',
|
||||
secret: 'sec-int-y9z0a1b2c3d4',
|
||||
status: 'disabled',
|
||||
createdAt: '2026-01-10',
|
||||
expiresAt: null,
|
||||
lastUsed: '2026-01-25 16:45:30',
|
||||
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)).map((g: FieldPermission) => ({
|
||||
...g,
|
||||
fields: g.fields.map(f => ({ ...f, enabled: true }))
|
||||
})),
|
||||
rateLimit: { requestsPerDay: -1, requestsPerMonth: -1 },
|
||||
billing: { plan: 'enterprise', usedCredits: 0, totalCredits: -1 },
|
||||
callStats: { today: 0, thisMonth: 567, total: 12345 }
|
||||
},
|
||||
]
|
||||
|
||||
export default function APIKeysPage() {
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>(MOCK_API_KEYS)
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState('')
|
||||
|
||||
// 弹窗状态
|
||||
const [showCreateKeyDialog, setShowCreateKeyDialog] = useState(false)
|
||||
const [showKeyDetailDialog, setShowKeyDetailDialog] = useState(false)
|
||||
const [showPermissionDialog, setShowPermissionDialog] = useState(false)
|
||||
const [selectedKey, setSelectedKey] = useState<APIKey | null>(null)
|
||||
|
||||
// 复制状态
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
|
||||
// 新密钥表单
|
||||
const [newKeyForm, setNewKeyForm] = useState<{
|
||||
name: string
|
||||
plan: 'free' | 'basic' | 'pro' | 'enterprise'
|
||||
expiresAt: string
|
||||
}>({
|
||||
name: '',
|
||||
plan: 'basic',
|
||||
expiresAt: '',
|
||||
})
|
||||
|
||||
// 显示/隐藏密钥
|
||||
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
const host = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
setApiBaseUrl(host)
|
||||
}, [])
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopiedId(id)
|
||||
setTimeout(() => setCopiedId(null), 2000)
|
||||
}
|
||||
|
||||
const toggleKeyVisibility = (keyId: string) => {
|
||||
const newSet = new Set(visibleKeys)
|
||||
if (newSet.has(keyId)) {
|
||||
newSet.delete(keyId)
|
||||
} else {
|
||||
newSet.add(keyId)
|
||||
}
|
||||
setVisibleKeys(newSet)
|
||||
}
|
||||
|
||||
const maskKey = (key: string) => {
|
||||
return key.substring(0, 12) + '••••••••••••'
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return <Badge className="bg-green-100 text-green-700">正常</Badge>
|
||||
case 'disabled': return <Badge className="bg-gray-100 text-gray-600">已禁用</Badge>
|
||||
case 'expired': return <Badge className="bg-red-100 text-red-700">已过期</Badge>
|
||||
default: return <Badge variant="outline">{status}</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getPlanBadge = (plan: string) => {
|
||||
switch (plan) {
|
||||
case 'free': return <Badge variant="outline">免费版</Badge>
|
||||
case 'basic': return <Badge className="bg-blue-100 text-blue-700">基础版</Badge>
|
||||
case 'pro': return <Badge className="bg-purple-100 text-purple-700">专业版</Badge>
|
||||
case 'enterprise': return <Badge className="bg-orange-100 text-orange-700">企业版</Badge>
|
||||
default: return <Badge variant="outline">{plan}</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新密钥
|
||||
const handleCreateKey = () => {
|
||||
const plan = BILLING_PLANS.find(p => p.id === newKeyForm.plan)!
|
||||
const newKey: APIKey = {
|
||||
id: `key_${Date.now()}`,
|
||||
name: newKeyForm.name,
|
||||
key: `sk-archer-${Math.random().toString(36).substring(2, 14)}`,
|
||||
secret: `sec-${Math.random().toString(36).substring(2, 14)}`,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
expiresAt: newKeyForm.expiresAt || null,
|
||||
lastUsed: null,
|
||||
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)),
|
||||
rateLimit: { requestsPerDay: plan.requestsPerDay, requestsPerMonth: plan.requestsPerMonth },
|
||||
billing: { plan: plan.id as 'free' | 'basic' | 'pro' | 'enterprise', usedCredits: 0, totalCredits: plan.credits },
|
||||
callStats: { today: 0, thisMonth: 0, total: 0 }
|
||||
}
|
||||
setApiKeys([...apiKeys, newKey])
|
||||
setShowCreateKeyDialog(false)
|
||||
setNewKeyForm({ name: '', plan: 'basic', expiresAt: '' })
|
||||
}
|
||||
|
||||
// 切换密钥状态
|
||||
const toggleKeyStatus = (keyId: string) => {
|
||||
setApiKeys(apiKeys.map(k =>
|
||||
k.id === keyId
|
||||
? { ...k, status: k.status === 'active' ? 'disabled' : 'active' }
|
||||
: k
|
||||
))
|
||||
}
|
||||
|
||||
// 删除密钥
|
||||
const deleteKey = (keyId: string) => {
|
||||
if (confirm('确定要删除此API密钥吗?此操作不可恢复。')) {
|
||||
setApiKeys(apiKeys.filter(k => k.id !== keyId))
|
||||
}
|
||||
}
|
||||
|
||||
// 更新字段权限
|
||||
const updateFieldPermission = (groupIndex: number, fieldIndex: number, enabled: boolean) => {
|
||||
if (!selectedKey) return
|
||||
const newPermissions = [...selectedKey.permissions]
|
||||
newPermissions[groupIndex].fields[fieldIndex].enabled = enabled
|
||||
setSelectedKey({ ...selectedKey, permissions: newPermissions })
|
||||
}
|
||||
|
||||
// 保存权限设置
|
||||
const savePermissions = () => {
|
||||
if (!selectedKey) return
|
||||
setApiKeys(apiKeys.map(k =>
|
||||
k.id === selectedKey.id ? selectedKey : k
|
||||
))
|
||||
setShowPermissionDialog(false)
|
||||
}
|
||||
|
||||
// 统计
|
||||
const activeKeysCount = apiKeys.filter(k => k.status === 'active').length
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 顶部标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/data-market/api">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
返回
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">API密钥管理</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">创建和管理第三方系统接入密钥</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setShowCreateKeyDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建密钥
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-purple-100">
|
||||
<Key className="h-5 w-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">{apiKeys.length}</div>
|
||||
<div className="text-sm text-gray-500">总密钥数</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-green-100">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">{activeKeysCount}</div>
|
||||
<div className="text-sm text-gray-500">正常使用</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-orange-100">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">{apiKeys.filter(k => k.status === 'disabled').length}</div>
|
||||
<div className="text-sm text-gray-500">已禁用</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-100">
|
||||
<Activity className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">{apiKeys.reduce((sum, k) => sum + k.callStats.today, 0).toLocaleString()}</div>
|
||||
<div className="text-sm text-gray-500">今日调用</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 认证说明 */}
|
||||
<Card className="border-0 shadow-sm bg-gradient-to-r from-purple-50 to-blue-50">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">认证方式</h3>
|
||||
<p className="text-sm text-gray-600 mb-3">所有API请求需要在Header中携带API密钥和密钥Secret:</p>
|
||||
<pre className="bg-gray-900 text-green-400 p-4 rounded-lg text-sm overflow-x-auto">
|
||||
{`curl -X GET "${apiBaseUrl}/api/shensheshou/user?phone=13800138000" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-H "X-API-Secret: YOUR_API_SECRET" \\
|
||||
-H "Content-Type: application/json"`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 密钥列表 */}
|
||||
<Card className="border-0 shadow-sm bg-white/80">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">密钥列表</CardTitle>
|
||||
<CardDescription>管理所有API密钥,配置权限和额度</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>API Key</TableHead>
|
||||
<TableHead>套餐</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>今日/本月调用</TableHead>
|
||||
<TableHead>信用点</TableHead>
|
||||
<TableHead>最后使用</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map(key => (
|
||||
<TableRow key={key.id}>
|
||||
<TableCell className="font-medium">{key.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono">
|
||||
{visibleKeys.has(key.id) ? key.key : maskKey(key.key)}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => toggleKeyVisibility(key.id)}
|
||||
>
|
||||
{visibleKeys.has(key.id) ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => copyToClipboard(key.key, key.id)}
|
||||
>
|
||||
{copiedId === key.id ? <CheckCircle2 className="h-3 w-3 text-green-500" /> : <Copy className="h-3 w-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getPlanBadge(key.billing.plan)}</TableCell>
|
||||
<TableCell>{getStatusBadge(key.status)}</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">{key.callStats.today.toLocaleString()} / {key.callStats.thisMonth.toLocaleString()}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-16 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-purple-500 rounded-full"
|
||||
style={{ width: key.billing.totalCredits === -1 ? '100%' : `${(key.billing.usedCredits / key.billing.totalCredits) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{key.billing.totalCredits === -1 ? '无限' : `${key.billing.usedCredits}/${key.billing.totalCredits}`}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">
|
||||
{key.lastUsed || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedKey(key)
|
||||
setShowKeyDetailDialog(true)
|
||||
}}
|
||||
title="密钥详情"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedKey(key)
|
||||
setShowPermissionDialog(true)
|
||||
}}
|
||||
title="权限配置"
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleKeyStatus(key.id)}
|
||||
title={key.status === 'active' ? '禁用' : '启用'}
|
||||
>
|
||||
{key.status === 'active' ? <Lock className="h-4 w-4" /> : <Unlock className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => deleteKey(key.id)}
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ==================== 弹窗 ==================== */}
|
||||
|
||||
{/* 创建密钥弹窗 */}
|
||||
<Dialog open={showCreateKeyDialog} onOpenChange={setShowCreateKeyDialog}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建API密钥</DialogTitle>
|
||||
<DialogDescription>为第三方系统创建新的API访问密钥</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>密钥名称 *</Label>
|
||||
<Input
|
||||
placeholder="例如:存客宝-生产环境"
|
||||
value={newKeyForm.name}
|
||||
onChange={e => setNewKeyForm({ ...newKeyForm, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>计费套餐</Label>
|
||||
<Select
|
||||
value={newKeyForm.plan}
|
||||
onValueChange={(v) => setNewKeyForm({ ...newKeyForm, plan: v as 'free' | 'basic' | 'pro' | 'enterprise' })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BILLING_PLANS.map(plan => (
|
||||
<SelectItem key={plan.id} value={plan.id}>
|
||||
{plan.name} - ¥{plan.price}/月 ({plan.credits === -1 ? '无限' : plan.credits.toLocaleString()} 信用点)
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>过期时间(可选)</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={newKeyForm.expiresAt}
|
||||
onChange={e => setNewKeyForm({ ...newKeyForm, expiresAt: e.target.value })}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">留空表示永不过期</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCreateKeyDialog(false)}>取消</Button>
|
||||
<Button onClick={handleCreateKey} disabled={!newKeyForm.name}>创建密钥</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 密钥详情弹窗 */}
|
||||
<Dialog open={showKeyDetailDialog} onOpenChange={setShowKeyDetailDialog}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>密钥详情</DialogTitle>
|
||||
<DialogDescription>{selectedKey?.name}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedKey && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>API Key</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={visibleKeys.has(selectedKey.id + '_detail') ? selectedKey.key : maskKey(selectedKey.key)}
|
||||
readOnly
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => toggleKeyVisibility(selectedKey.id + '_detail')}
|
||||
>
|
||||
{visibleKeys.has(selectedKey.id + '_detail') ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => copyToClipboard(selectedKey.key, selectedKey.id + '_key')}
|
||||
>
|
||||
{copiedId === selectedKey.id + '_key' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>API Secret</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={visibleKeys.has(selectedKey.id + '_secret') ? selectedKey.secret : '••••••••••••••••'}
|
||||
readOnly
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => toggleKeyVisibility(selectedKey.id + '_secret')}
|
||||
>
|
||||
{visibleKeys.has(selectedKey.id + '_secret') ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => copyToClipboard(selectedKey.secret, selectedKey.id + '_sec')}
|
||||
>
|
||||
{copiedId === selectedKey.id + '_sec' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-gray-500">创建时间</Label>
|
||||
<p className="font-medium">{selectedKey.createdAt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-500">过期时间</Label>
|
||||
<p className="font-medium">{selectedKey.expiresAt || '永不过期'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-500">计费套餐</Label>
|
||||
<p>{getPlanBadge(selectedKey.billing.plan)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-500">状态</Label>
|
||||
<p>{getStatusBadge(selectedKey.status)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-gray-50">
|
||||
<h4 className="font-medium mb-2">调用统计</h4>
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-xl font-bold text-gray-900">{selectedKey.callStats.today.toLocaleString()}</div>
|
||||
<div className="text-xs text-gray-500">今日调用</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-gray-900">{selectedKey.callStats.thisMonth.toLocaleString()}</div>
|
||||
<div className="text-xs text-gray-500">本月调用</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-gray-900">{selectedKey.callStats.total.toLocaleString()}</div>
|
||||
<div className="text-xs text-gray-500">总调用</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-yellow-50 text-sm text-yellow-700">
|
||||
⚠️ 请妥善保管API密钥,不要在客户端代码中暴露
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowKeyDetailDialog(false)}>关闭</Button>
|
||||
<Button variant="destructive">
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
重新生成
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 字段权限配置弹窗 */}
|
||||
<Dialog open={showPermissionDialog} onOpenChange={setShowPermissionDialog}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>字段权限配置</DialogTitle>
|
||||
<DialogDescription>配置 {selectedKey?.name} 可访问的数据字段</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedKey && (
|
||||
<div className="space-y-6 py-4">
|
||||
{selectedKey.permissions.map((group, groupIndex) => (
|
||||
<div key={group.fieldGroup} className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-gray-900">{group.fieldGroup}</h4>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const allEnabled = group.fields.every(f => f.enabled)
|
||||
const newPermissions = [...selectedKey.permissions]
|
||||
newPermissions[groupIndex].fields = newPermissions[groupIndex].fields.map(f => ({ ...f, enabled: !allEnabled }))
|
||||
setSelectedKey({ ...selectedKey, permissions: newPermissions })
|
||||
}}
|
||||
>
|
||||
{group.fields.every(f => f.enabled) ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{group.fields.map((field, fieldIndex) => (
|
||||
<div
|
||||
key={field.name}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border ${field.enabled ? 'bg-purple-50 border-purple-200' : 'bg-gray-50 border-gray-200'}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={field.enabled}
|
||||
onCheckedChange={(checked) => updateFieldPermission(groupIndex, fieldIndex, checked as boolean)}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{field.label}</div>
|
||||
<div className="text-xs text-gray-500">{field.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{field.price} 点/次
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="p-4 rounded-lg bg-blue-50 text-sm text-blue-700">
|
||||
💡 调用API时,每访问一个字段都会按对应价格扣除信用点。未授权的字段在API响应中将被过滤。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowPermissionDialog(false)}>取消</Button>
|
||||
<Button onClick={savePermissions}>保存配置</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
147
app/page.tsx
@@ -33,8 +33,12 @@ interface ChatMessage {
|
||||
content: string
|
||||
timestamp: string
|
||||
data?: any
|
||||
thinking?: string // AI思考过程
|
||||
portrait?: UserPortrait // 用户画像
|
||||
thinking?: string
|
||||
portrait?: UserPortrait
|
||||
fullProfile?: { valuation?: any; qqPhone?: any; ckbAsset?: any }
|
||||
aiAnalysis?: string
|
||||
/** 多人列表(最多 10 条完整画像) */
|
||||
profiles?: { fullProfile: any; aiAnalysis: string }[]
|
||||
}
|
||||
|
||||
// 标准用户画像模板
|
||||
@@ -72,7 +76,7 @@ export default function HomePage() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
role: "assistant",
|
||||
content: "你好!我是神射手AI助手。\n\n你可以:\n• 输入手机号查询用户画像\n• 输入QQ号查询关联信息\n• 问我任何关于用户数据的问题",
|
||||
content: "你好!我是神射手AI助手。\n\n支持 **手机、QQ、身份证、姓名** 统一搜索,曼谷库内数据完整展示:\n• 可搜一人或多人,用逗号分隔,最多列出 **前 10 条** 完整画像\n• 每条含:手机、QQ、地址、统一标签、流量池、存客宝、AI 分析\n• 也可问系统状态、高价值用户、RFM 等",
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
}
|
||||
])
|
||||
@@ -107,24 +111,26 @@ export default function HomePage() {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}, [messages])
|
||||
|
||||
// 解析用户画像数据
|
||||
// 解析用户画像(支持 fullProfile 与旧格式)
|
||||
const parsePortrait = (data: any): UserPortrait | undefined => {
|
||||
if (!data) return undefined
|
||||
const v = data.fullProfile?.valuation || data.valuation || data
|
||||
const qq = data.fullProfile?.qqPhone || data.qqPhone
|
||||
return {
|
||||
phone: data.phone || data.手机号,
|
||||
qq: data.qq || data.QQ,
|
||||
name: data.name || data.姓名,
|
||||
gender: data.gender || data.性别,
|
||||
province: data.province || data.省份,
|
||||
city: data.city || data.城市,
|
||||
level: data.level || data.user_level || data.等级,
|
||||
rfmScore: data.rfm_score || data.rfmScore || data.RFM评分,
|
||||
tags: data.tags || [],
|
||||
phone: v.phone_masked || v.phone || qq?.phone || data.手机号,
|
||||
qq: v.qq || qq?.qq || data.qq || data.QQ,
|
||||
name: v.name || data.姓名,
|
||||
gender: v.gender || data.性别,
|
||||
province: v.province || qq?.省份 || data.省份,
|
||||
city: v.city || qq?.地区 || data.城市,
|
||||
level: v.user_level || data.user_level || data.等级,
|
||||
rfmScore: v.user_evaluation_score ?? data.rfm_score ?? data.user_evaluation_score,
|
||||
tags: v.unified_tags || v.tags || data.tags || [],
|
||||
behavior: {
|
||||
lastActive: data.last_active || data.最近活跃,
|
||||
frequency: data.frequency || data.活跃频率,
|
||||
lastActive: v.last_active || data.最近活跃,
|
||||
frequency: v.frequency || data.活跃频率,
|
||||
},
|
||||
dataCompleteness: data.data_completeness || data.数据完整度,
|
||||
dataCompleteness: v.data_completeness || data.数据完整度,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,14 +138,13 @@ export default function HomePage() {
|
||||
const generateThinking = (query: string): string => {
|
||||
const isPhone = /^1[3-9]\d{9}$/.test(query.replace(/\s/g, ''))
|
||||
const isQQ = /^\d{5,11}$/.test(query.replace(/\s|qq/gi, ''))
|
||||
|
||||
if (isPhone) {
|
||||
return `🔍 识别到手机号查询\n→ 拆解意图: 查询用户画像\n→ 数据源: KR.用户估值, KR_腾讯\n→ 执行: 手机号精确匹配\n→ 输出: 标准用户画像模板`
|
||||
} else if (isQQ) {
|
||||
return `🔍 识别到QQ号查询\n→ 拆解意图: QQ关联查询\n→ 数据源: KR_腾讯.qq_phone\n→ 执行: QQ号精确匹配\n→ 输出: 关联手机号 + 用户画像`
|
||||
} else {
|
||||
return `🔍 自然语言理解中...\n→ 拆解意图: ${query.slice(0, 20)}...\n→ 思考策略: 语义分析\n→ 执行: Skill查询`
|
||||
return `🔍 识别到手机号\n→ 意图: 完整用户画像\n→ 数据源: KR + KR_腾讯 + KR_存客宝\n→ 输出: 完整画像 + AI 分析`
|
||||
}
|
||||
if (isQQ) {
|
||||
return `🔍 识别到 QQ 号\n→ 意图: 完整用户画像\n→ 数据源: KR_腾讯 → 手机号 → 跨库画像\n→ 输出: 完整画像 + AI 分析`
|
||||
}
|
||||
return `🔍 关键字搜索\n→ 意图: 任意关键词匹配\n→ 数据源: KR.用户估值(姓名/城市/省份)\n→ 输出: 第一条完整画像 + AI 分析`
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
@@ -176,14 +181,19 @@ export default function HomePage() {
|
||||
const portrait = parsePortrait(data.response?.data)
|
||||
|
||||
setMessages(prev => {
|
||||
// 移除思考消息,添加结果
|
||||
const filtered = prev.filter(m => m.role !== "thinking")
|
||||
const respData = data.response?.data
|
||||
const profiles = respData?.profiles || respData?.list
|
||||
const first = Array.isArray(profiles)?.[0]
|
||||
return [...filtered, {
|
||||
role: "assistant",
|
||||
content: data.success ? data.response.content : `查询失败: ${data.error || "未知错误"}`,
|
||||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||||
data: data.response?.data,
|
||||
portrait,
|
||||
data: respData,
|
||||
portrait: portrait || (first ? parsePortrait(first) : undefined),
|
||||
fullProfile: respData?.fullProfile || first?.fullProfile,
|
||||
aiAnalysis: respData?.aiAnalysis || first?.aiAnalysis,
|
||||
profiles: Array.isArray(profiles) ? profiles : undefined,
|
||||
thinking: generateThinking(query),
|
||||
}]
|
||||
})
|
||||
@@ -350,10 +360,57 @@ export default function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户画像卡片 */}
|
||||
{msg.portrait && (msg.portrait.phone || msg.portrait.qq) && (
|
||||
<div className="rounded-xl p-3 bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-100">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{/* 多人列表:前 10 条完整画像(手机、QQ、地址、统一标签、AI 分析) */}
|
||||
{msg.profiles && msg.profiles.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{msg.profiles.map((item, idx) => {
|
||||
const p = parsePortrait(item)
|
||||
const fp = item.fullProfile
|
||||
const addr = [fp?.valuation?.province, fp?.valuation?.city, fp?.qqPhone?.省份, fp?.qqPhone?.地区].filter(Boolean)
|
||||
const tags = [...(fp?.valuation?.unified_tags || []), ...(fp?.valuation?.tags || []), ...(fp?.ckbAsset?.tags || [])]
|
||||
const uniqTags = [...new Set(tags)]
|
||||
return (
|
||||
<div key={idx} className="rounded-xl p-3 bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-100 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-9 h-9 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center text-white text-sm font-bold">
|
||||
{(p?.name || '?')[0]}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-gray-900">{p?.name || '未知用户'}</div>
|
||||
<div className="text-xs text-gray-500 flex flex-wrap gap-x-2">
|
||||
{p?.phone && <span>📱 {p.phone}</span>}
|
||||
{p?.qq && <span>QQ: {p.qq}</span>}
|
||||
{addr.length > 0 && <span>📍 {addr.join(' ')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{p?.level && <Badge className="shrink-0 bg-purple-100 text-purple-700">{p.level}级</Badge>}
|
||||
{p?.rfmScore != null && <span className="text-xs text-purple-600 font-medium">{p.rfmScore} 分</span>}
|
||||
</div>
|
||||
{fp?.valuation?.traffic_pool?.pool_name && (
|
||||
<div className="text-xs text-gray-600">📦 流量池: {fp.valuation.traffic_pool.pool_name}</div>
|
||||
)}
|
||||
{uniqTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{uniqTags.slice(0, 8).map((tag, ti) => (
|
||||
<Badge key={ti} variant="secondary" className="text-xs">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{item.aiAnalysis && (
|
||||
<div className="rounded-lg p-2 bg-white/90 border border-purple-100">
|
||||
<div className="text-xs text-purple-600 font-medium mb-0.5">🤖 AI 分析</div>
|
||||
<div className="text-xs text-gray-700">{item.aiAnalysis}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{/* 单人完整用户画像卡片(含 AI 分析) */}
|
||||
{!msg.profiles?.length && msg.portrait && (msg.portrait.phone || msg.portrait.qq || msg.fullProfile) && (
|
||||
<div className="rounded-xl p-3 bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-100 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center text-white font-bold">
|
||||
{(msg.portrait.name || '?')[0]}
|
||||
</div>
|
||||
@@ -371,7 +428,7 @@ export default function HomePage() {
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
{msg.portrait.rfmScore !== undefined && (
|
||||
<div className="p-2 rounded bg-white/80">
|
||||
<div className="text-gray-500">RFM</div>
|
||||
<div className="text-gray-500">估值分</div>
|
||||
<div className="font-bold text-purple-600">{msg.portrait.rfmScore}</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -381,20 +438,33 @@ export default function HomePage() {
|
||||
<div className="font-medium">{msg.portrait.province} {msg.portrait.city}</div>
|
||||
</div>
|
||||
)}
|
||||
{msg.portrait.dataCompleteness !== undefined && (
|
||||
<div className="p-2 rounded bg-white/80">
|
||||
<div className="text-gray-500">完整度</div>
|
||||
<div className="font-medium text-green-600">{msg.portrait.dataCompleteness}%</div>
|
||||
{msg.fullProfile?.qqPhone && (
|
||||
<div className="p-2 rounded bg-white/80 col-span-1">
|
||||
<div className="text-gray-500">运营商</div>
|
||||
<div className="font-medium">{msg.fullProfile.qqPhone.运营商 || '—'}</div>
|
||||
</div>
|
||||
)}
|
||||
{msg.fullProfile?.ckbAsset && (
|
||||
<div className="p-2 rounded bg-white/80 col-span-3">
|
||||
<div className="text-gray-500">存客宝</div>
|
||||
<div className="font-medium">{msg.fullProfile.ckbAsset.nickname || '—'} · 资产 {msg.fullProfile.ckbAsset.total_assets ?? '—'}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{msg.portrait.tags && msg.portrait.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{msg.portrait.tags.slice(0, 5).map((tag, ti) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs text-gray-500 mr-1">标签:</span>
|
||||
{msg.portrait.tags.slice(0, 8).map((tag, ti) => (
|
||||
<Badge key={ti} variant="secondary" className="text-xs">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{msg.aiAnalysis && (
|
||||
<div className="rounded-lg p-2.5 bg-white/90 border border-purple-100">
|
||||
<div className="text-xs text-purple-600 font-medium mb-1">🤖 AI 分析</div>
|
||||
<div className="text-xs text-gray-700 whitespace-pre-wrap">{msg.aiAnalysis.replace(/\*\*/g, '')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -430,6 +500,9 @@ export default function HomePage() {
|
||||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("系统状态")}>
|
||||
<Activity className="w-3 h-3 mr-1" /> 系统状态
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("深圳")}>
|
||||
<Search className="w-3 h-3 mr-1" /> 关键词
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("高价值用户")}>
|
||||
<TrendingUp className="w-3 h-3 mr-1" /> 高价值
|
||||
</Button>
|
||||
@@ -443,7 +516,7 @@ export default function HomePage() {
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSend()}
|
||||
placeholder="输入手机号、QQ号或问题..."
|
||||
placeholder="输入手机、QQ、身份证、姓名(可多条逗号分隔,最多前10条)..."
|
||||
className="flex-1 h-10 bg-gray-50 border-0"
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Line } from "@ant-design/plots"
|
||||
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart as RechartsPieChart, Pie, Cell, Legend } from "recharts"
|
||||
|
||||
interface LineChartProps {
|
||||
data: { date: string; value: number }[]
|
||||
@@ -45,3 +46,57 @@ export function LineChart({ data, xField, yField }: LineChartProps) {
|
||||
|
||||
return <Line {...config} />
|
||||
}
|
||||
|
||||
// 图表 data 格式:{ labels, datasets: [{ data, backgroundColor?, label? }] }
|
||||
interface ChartDataProps {
|
||||
data: {
|
||||
labels: string[]
|
||||
datasets: { data: number[]; backgroundColor?: string | string[]; label?: string }[]
|
||||
}
|
||||
height?: number
|
||||
}
|
||||
|
||||
export function PieChart({ data, height = 200 }: ChartDataProps) {
|
||||
const labels = data.labels || []
|
||||
const first = data.datasets?.[0]
|
||||
const values = first?.data || []
|
||||
const colors = Array.isArray(first?.backgroundColor) ? first.backgroundColor : values.map(() => "hsl(var(--primary))")
|
||||
const rechartsData = labels.map((name, i) => ({ name, value: values[i] ?? 0, color: colors[i] ?? "hsl(var(--primary))" }))
|
||||
|
||||
return (
|
||||
<div style={{ height }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsPieChart>
|
||||
<Pie data={rechartsData} cx="50%" cy="50%" innerRadius={50} outerRadius={80} paddingAngle={2} dataKey="value">
|
||||
{rechartsData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</RechartsPieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function BarChart({ data, height = 200 }: ChartDataProps) {
|
||||
const labels = data.labels || []
|
||||
const first = data.datasets?.[0]
|
||||
const values = first?.data || []
|
||||
const rechartsData = labels.map((label, i) => ({ name: label, value: values[i] ?? 0 }))
|
||||
const fill = (typeof first?.backgroundColor === "string" ? first.backgroundColor : "hsl(var(--primary))") as string
|
||||
|
||||
return (
|
||||
<div style={{ height }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsBarChart data={rechartsData} margin={{ top: 8, right: 8, left: 8, bottom: 8 }}>
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="value" fill={fill} />
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
2
components/charts/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// 统一导出,供 @/components/charts 引用
|
||||
export { LineChart, PieChart, BarChart } from "../Charts"
|
||||
20
docker-compose.nas.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
# 神射手 - 公司 NAS (ckbnas 192.168.1.201) 部署
|
||||
# 应用与数据库均在同一台 NAS:数据库使用 NAS 上已有的 MongoDB 容器
|
||||
# 使用方式(在 NAS 上):
|
||||
# docker compose -f docker-compose.nas.yml up -d
|
||||
# 或本机构建后推送到 NAS 再 up。
|
||||
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
shensheshou:
|
||||
build: .
|
||||
image: shensheshou:latest
|
||||
container_name: shensheshou
|
||||
environment:
|
||||
# 使用 NAS 本机 MongoDB(与群晖NAS管理 SKILL 一致)
|
||||
MONGODB_URI: "mongodb://admin:admin123@192.168.1.201:27017/?authSource=admin"
|
||||
NODE_ENV: production
|
||||
ports:
|
||||
- "3117:3117"
|
||||
restart: unless-stopped
|
||||
48
docker-compose.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
# 所有网站类服务统一放在 project name: website 下
|
||||
# 只使用唯一 MongoDB:datacenter_mongodb(宿主机 27017),不在此编排中新建 MongoDB
|
||||
# 使用方式:在神射手目录执行 docker compose up -d
|
||||
# 神射手:http://localhost:3117 玩值电竞:http://localhost:3001
|
||||
|
||||
name: website
|
||||
|
||||
services:
|
||||
shensheshou:
|
||||
build: .
|
||||
image: shensheshou:latest
|
||||
container_name: website-shensheshou
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: "3117"
|
||||
# 唯一 MongoDB:datacenter_mongodb(宿主机 27017)
|
||||
MONGODB_URI: "mongodb://host.docker.internal:27017"
|
||||
ports:
|
||||
- "3117:3117"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- website
|
||||
|
||||
# 玩值电竞 Web:与神射手同属 website,共用唯一 MongoDB(datacenter_mongodb 27017)
|
||||
wanzhi-web:
|
||||
build:
|
||||
context: ../../3、自营项目/玩值电竞App
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
BUILD_TIME: ${BUILD_TIME:-}
|
||||
image: wanzhi-app:latest
|
||||
container_name: website-wanzhi-web
|
||||
environment:
|
||||
MONGODB_URI: mongodb://host.docker.internal:27017
|
||||
NODE_ENV: production
|
||||
ports:
|
||||
- "3001:3000"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- website
|
||||
|
||||
networks:
|
||||
website:
|
||||
driver: bridge
|
||||
116
lib/mongodb.ts
@@ -1,12 +1,22 @@
|
||||
/**
|
||||
* MongoDB 连接器 - 神射手用户资产数字化查询
|
||||
* 连接本地 Docker MongoDB,支持 20亿+ 用户数据查询
|
||||
* 连接本地/Docker MongoDB,支持 20亿+ 用户数据查询
|
||||
* 账号密码见卡若AI《00_账号与API索引》二、本机 MongoDB(统一):admin / admin123
|
||||
*/
|
||||
|
||||
import { MongoClient, Db, Collection, ObjectId } from 'mongodb'
|
||||
|
||||
// MongoDB 连接配置 - 本地 Docker
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://admin:admin123@localhost:27017/?authSource=admin'
|
||||
// 本地开发默认(有认证):卡若AI 本机 MongoDB 账号 admin/admin123
|
||||
const DEFAULT_MONGODB_URI = 'mongodb://admin:admin123@localhost:27017/?authSource=admin'
|
||||
|
||||
// 优先使用环境变量;未设置时才用默认(Docker 下 MONGODB_URI 通常为 host.docker.internal 或无认证,不要被替换成 localhost)
|
||||
function getMongoUri(): string {
|
||||
const env = process.env.MONGODB_URI?.trim()
|
||||
if (!env) return DEFAULT_MONGODB_URI
|
||||
return env
|
||||
}
|
||||
|
||||
const MONGODB_URI = getMongoUri()
|
||||
|
||||
// 数据库和集合常量
|
||||
const DB_NAMES = {
|
||||
@@ -81,10 +91,16 @@ export async function getMongoClient(): Promise<MongoClient> {
|
||||
maxPoolSize: 10,
|
||||
minPoolSize: 2,
|
||||
maxIdleTimeMS: 60000,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
serverSelectionTimeoutMS: 15000,
|
||||
connectTimeoutMS: 10000,
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
try {
|
||||
await client.connect()
|
||||
} catch (err: any) {
|
||||
cachedClient = null
|
||||
throw new Error(`数据库连接失败: ${err.message}(请确认 MongoDB 已启动且 MONGODB_URI 正确)`)
|
||||
}
|
||||
cachedClient = client
|
||||
return client
|
||||
}
|
||||
@@ -351,6 +367,96 @@ export async function intelligentSearch(query: string, options: {
|
||||
return { users, total, queryType }
|
||||
}
|
||||
|
||||
/** 身份证号规范化(18 位或 17 位+X) */
|
||||
function normalizeIdCard(idCard: string): string {
|
||||
const s = idCard.replace(/\s/g, '').toUpperCase()
|
||||
if (/^\d{17}[\dXx]$/.test(s)) return s
|
||||
if (/^\d{18}$/.test(s)) return s
|
||||
return idCard
|
||||
}
|
||||
|
||||
/**
|
||||
* 按身份证号查询用户(KR.用户估值 中 id_card / 身份证 字段,若有)
|
||||
* 返回手机号以便后续拉取完整画像
|
||||
*/
|
||||
export async function searchByIdCard(idCard: string): Promise<{ phone?: string; valuation?: UserValuationDoc } | null> {
|
||||
const client = await getMongoClient()
|
||||
const col = client.db(DB_NAMES.KR).collection<UserValuationDoc & { id_card?: string; 身份证?: string }>('用户估值')
|
||||
const normalized = normalizeIdCard(idCard)
|
||||
const doc = await col.findOne({
|
||||
$or: [
|
||||
{ id_card: normalized },
|
||||
{ 身份证: normalized },
|
||||
{ id_card: idCard },
|
||||
{ 身份证: idCard }
|
||||
]
|
||||
} as any)
|
||||
if (!doc) return null
|
||||
const phone = doc.phone || (doc as any).phone_masked
|
||||
return phone ? { phone: String(phone).replace(/\D/g, '').slice(-11) || String(phone), valuation: doc } : null
|
||||
}
|
||||
|
||||
/** 将多种标识统一解析为手机号列表(去重,最多 max 条) */
|
||||
export async function unifiedResolveToPhones(segments: { type: 'phone' | 'qq' | 'id_card' | 'keyword'; value: string }[], max: number = 10): Promise<string[]> {
|
||||
const client = await getMongoClient()
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
|
||||
for (const seg of segments) {
|
||||
if (result.length >= max) break
|
||||
const v = seg.value.trim()
|
||||
if (!v) continue
|
||||
|
||||
if (seg.type === 'phone') {
|
||||
const cleaned = v.replace(/\D/g, '').replace(/^86/, '')
|
||||
const phone11 = cleaned.length >= 11 ? cleaned.slice(-11) : cleaned
|
||||
if (/^1[3-9]\d{9}$/.test(phone11) && !seen.has(phone11)) {
|
||||
seen.add(phone11)
|
||||
result.push(phone11)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (seg.type === 'qq') {
|
||||
const qqDoc = await client.db(DB_NAMES.KR_腾讯).collection<QQPhoneDoc>('QQ+手机').findOne({
|
||||
$or: [{ qq: v }, { qq: parseInt(v) }]
|
||||
})
|
||||
const phone = (qqDoc?.phone || qqDoc?.手机号)?.toString?.()
|
||||
if (phone) {
|
||||
const p11 = phone.replace(/\D/g, '').slice(-11)
|
||||
if (/^1[3-9]\d{9}$/.test(p11) && !seen.has(p11)) {
|
||||
seen.add(p11)
|
||||
result.push(p11)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (seg.type === 'id_card') {
|
||||
const idRes = await searchByIdCard(v)
|
||||
if (idRes?.phone && !seen.has(idRes.phone)) {
|
||||
seen.add(idRes.phone)
|
||||
result.push(idRes.phone)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (seg.type === 'keyword') {
|
||||
const searchRes = await intelligentSearch(v, { limit: max - result.length })
|
||||
for (const u of searchRes.users) {
|
||||
if (result.length >= max) break
|
||||
const phone = u.phone?.toString?.().replace(/\D/g, '').slice(-11)
|
||||
if (phone && /^1[3-9]\d{9}$/.test(phone) && !seen.has(phone)) {
|
||||
seen.add(phone)
|
||||
result.push(phone)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.slice(0, max)
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"playwright": "^1.58.2",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
|
||||
29
pnpm-lock.yaml
generated
@@ -165,6 +165,9 @@ importers:
|
||||
eslint-config-next:
|
||||
specifier: 14.1.0
|
||||
version: 14.1.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
playwright:
|
||||
specifier: ^1.58.2
|
||||
version: 1.58.2
|
||||
postcss:
|
||||
specifier: ^8
|
||||
version: 8.5.6
|
||||
@@ -1966,6 +1969,11 @@ packages:
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -2566,6 +2574,16 @@ packages:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
playwright-core@1.58.2:
|
||||
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.58.2:
|
||||
resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5173,6 +5191,9 @@ snapshots:
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -5755,6 +5776,14 @@ snapshots:
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
playwright-core@1.58.2: {}
|
||||
|
||||
playwright@1.58.2:
|
||||
dependencies:
|
||||
playwright-core: 1.58.2
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.5.6):
|
||||
|
||||
40
scripts/deploy-to-nas.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# 神射手 - 一键部署到公司 NAS (ckbnas 192.168.1.201)
|
||||
# 功能:rsync 同步代码 → NAS 上 docker compose 构建并启动
|
||||
# 数据库:使用 NAS 上已有 MongoDB(192.168.1.201:27017)
|
||||
# 用法:与 NAS 同网下执行 ./scripts/deploy-to-nas.sh
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
NAS_HOST="${NAS_HOST:-192.168.1.201}"
|
||||
NAS_USER="${NAS_USER:-fnvtk}"
|
||||
NAS_PATH="${NAS_PATH:-/volume1/docker/shensheshou}"
|
||||
DOCKER="/volume1/@appstore/ContainerManager/usr/bin/docker"
|
||||
|
||||
echo "==> 神射手部署到 NAS ($NAS_USER@$NAS_HOST)"
|
||||
echo " 目标路径: $NAS_PATH"
|
||||
echo ""
|
||||
|
||||
# 1. 同步代码(排除 node_modules、.git、.next)
|
||||
echo "==> 同步代码到 NAS..."
|
||||
rsync -avz --progress \
|
||||
--exclude='node_modules' \
|
||||
--exclude='.git' \
|
||||
--exclude='.next' \
|
||||
--exclude='.env.local' \
|
||||
--exclude='*.log' \
|
||||
./ "${NAS_USER}@${NAS_HOST}:${NAS_PATH}/"
|
||||
|
||||
# 2. 在 NAS 上构建并启动
|
||||
echo ""
|
||||
echo "==> 在 NAS 上构建并启动容器..."
|
||||
ssh -o ConnectTimeout=10 "${NAS_USER}@${NAS_HOST}" "cd ${NAS_PATH} && \
|
||||
${DOCKER} compose -f docker-compose.nas.yml build --no-cache && \
|
||||
echo 'zhiqun1984' | sudo -S ${DOCKER} compose -f docker-compose.nas.yml up -d"
|
||||
|
||||
echo ""
|
||||
echo "==> 部署完成"
|
||||
echo " 访问: http://${NAS_HOST}:3117"
|
||||
echo " 健康检查: curl -s http://${NAS_HOST}:3117/api/monitoring?action=health"
|
||||
echo " 查看日志: ssh ${NAS_USER}@${NAS_HOST} '${DOCKER} logs -f shensheshou'"
|
||||
71
scripts/launch_local.sh
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/bin/zsh
|
||||
# 神射手 本地版启动脚本 - 纯 shell,无 Python/tkinter 依赖
|
||||
|
||||
PROJECT="/Users/karuo/Documents/开发/2、私域银行/神射手"
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.volta/bin:${PATH}"
|
||||
[ -d "${HOME}/.fnm/current/bin" ] && export PATH="${HOME}/.fnm/current/bin:${PATH}"
|
||||
|
||||
notify() { osascript -e "display notification \"$1\" with title \"神射手\"" 2>/dev/null; }
|
||||
open_url() { open "$1" 2>/dev/null || true; }
|
||||
|
||||
cd "$PROJECT" || { notify "项目路径不存在"; exit 1; }
|
||||
|
||||
# 1. Ollama
|
||||
notify "检查本地大模型..."
|
||||
if ! curl -s --connect-timeout 2 "http://localhost:11434/api/tags" >/dev/null 2>&1; then
|
||||
notify "正在启动 Ollama..."
|
||||
open -a Ollama 2>/dev/null || true
|
||||
for i in {1..45}; do
|
||||
curl -s --connect-timeout 2 "http://localhost:11434/api/tags" >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
notify "Ollama 就绪"
|
||||
|
||||
# 2. Docker
|
||||
notify "检查 Docker..."
|
||||
open -a Docker 2>/dev/null || true
|
||||
for i in {1..60}; do
|
||||
docker info >/dev/null 2>&1 && break
|
||||
sleep 2
|
||||
done
|
||||
notify "Docker 就绪"
|
||||
|
||||
# 3. MongoDB
|
||||
notify "启动 MongoDB..."
|
||||
docker start datacenter_mongodb 2>/dev/null || true
|
||||
sleep 3
|
||||
notify "MongoDB 就绪"
|
||||
|
||||
# 4. 若 3117 已监听,直接打开
|
||||
if lsof -i :3117 -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
notify "服务已在运行,打开浏览器"
|
||||
open_url "http://localhost:3117"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 5. 启动 pnpm dev(后台)
|
||||
notify "启动神射手开发服务..."
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
pnpm dev &
|
||||
else
|
||||
npm run dev &
|
||||
fi
|
||||
PID=$!
|
||||
trap "kill $PID 2>/dev/null; exit" EXIT INT TERM
|
||||
|
||||
# 6. 等待 3117
|
||||
for i in {1..90}; do
|
||||
lsof -i :3117 -sTCP:LISTEN >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 7. 打开浏览器
|
||||
notify "神射手已启动"
|
||||
open_url "http://localhost:3117"
|
||||
echo "神射手已启动: http://localhost:3117"
|
||||
echo "关闭此窗口将停止服务"
|
||||
echo ""
|
||||
|
||||
# 8. 保持运行
|
||||
wait $PID
|
||||
7
scripts/launch_nas.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/zsh
|
||||
# 神射手 NAS 版启动脚本 - 打开 192.168.1.201:3117
|
||||
|
||||
NAS_URL="http://192.168.1.201:3117"
|
||||
osascript -e "display notification \"正在打开神射手 (NAS)\" with title \"神射手\"" 2>/dev/null
|
||||
open "$NAS_URL"
|
||||
exit 0
|
||||
287
scripts/launch_progress.py
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
神射手 桌面启动器 - 带进度条
|
||||
- 本地版:Ollama → Docker → MongoDB → pnpm dev → 打开浏览器
|
||||
- NAS版(--nas):检查 NAS 可达性 → 打开 http://192.168.1.201:3117
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import signal
|
||||
import threading
|
||||
|
||||
USE_GUI = True
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
except ImportError:
|
||||
USE_GUI = False
|
||||
|
||||
PROJECT = "/Users/karuo/Documents/开发/2、私域银行/神射手"
|
||||
import glob
|
||||
PATH_DIRS = [
|
||||
"/opt/homebrew/bin", "/usr/local/bin",
|
||||
os.path.expanduser("~/.volta/bin"),
|
||||
]
|
||||
for d in PATH_DIRS:
|
||||
if os.path.isdir(d):
|
||||
os.environ["PATH"] = d + os.pathsep + os.environ.get("PATH", "")
|
||||
for p in glob.glob(os.path.expanduser("~/.fnm/*/installation/bin")):
|
||||
if os.path.isdir(p):
|
||||
os.environ["PATH"] = p + os.pathsep + os.environ.get("PATH", "")
|
||||
for p in glob.glob(os.path.expanduser("~/.nvm/versions/node/*/bin")):
|
||||
if os.path.isdir(p):
|
||||
os.environ["PATH"] = p + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
server_pid = None
|
||||
_widgets = {} # 存放控件引用
|
||||
NAS_MODE = "--nas" in sys.argv or "-n" in sys.argv
|
||||
NAS_URL = "http://192.168.1.201:3117"
|
||||
|
||||
|
||||
def notify(msg):
|
||||
"""发送 macOS 通知(无 GUI 时使用)"""
|
||||
subprocess.run(
|
||||
["osascript", "-e", f'display notification "{msg}" with title "神射手"'],
|
||||
capture_output=True, timeout=2
|
||||
)
|
||||
|
||||
|
||||
def set_progress(pct, step, msg):
|
||||
"""线程安全更新进度"""
|
||||
if not USE_GUI:
|
||||
return
|
||||
w = _widgets
|
||||
if "pct" in w:
|
||||
w["pct"]["text"] = f"{pct}%"
|
||||
if "step" in w:
|
||||
w["step"]["text"] = step
|
||||
if "msg" in w:
|
||||
w["msg"]["text"] = msg
|
||||
if "progress" in w:
|
||||
w["progress"]["value"] = pct
|
||||
if "root" in w:
|
||||
try:
|
||||
w["root"].update_idletasks()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def port_listen(port):
|
||||
"""检测端口是否在监听"""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["lsof", "-i", f":{port}", "-sTCP:LISTEN"],
|
||||
capture_output=True, timeout=2
|
||||
)
|
||||
return r.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def http_ok(url, timeout=2):
|
||||
"""检测 HTTP 是否可访问"""
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(url)
|
||||
urllib.request.urlopen(req, timeout=timeout)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def run_steps_nas(root, start_time):
|
||||
"""NAS 版:检查 NAS 可达性 → 打开浏览器"""
|
||||
def elapsed():
|
||||
return int(time.time() - start_time)
|
||||
|
||||
def upd(pct, step, msg):
|
||||
full = f"{msg} (已用时 {elapsed()} 秒)"
|
||||
set_progress(pct, step, full)
|
||||
if not USE_GUI:
|
||||
notify(f"{pct}% | {step}: {msg}")
|
||||
|
||||
upd(0, "神射手 (NAS)", "检查 NAS 连通性...")
|
||||
if http_ok(NAS_URL, timeout=5):
|
||||
upd(90, "神射手 (NAS)", "NAS 服务可用")
|
||||
else:
|
||||
upd(50, "神射手 (NAS)", "NAS 可能未启动,仍尝试打开...")
|
||||
time.sleep(1)
|
||||
upd(100, "完成", "正在打开浏览器...")
|
||||
subprocess.run(["open", NAS_URL], timeout=3)
|
||||
time.sleep(2)
|
||||
# NAS 版无后台进程,2 秒后自动关闭窗口
|
||||
if root is not None and "root" in _widgets:
|
||||
try:
|
||||
r = _widgets["root"]
|
||||
r.after(0, r.destroy)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_steps(root, start_time):
|
||||
"""root 为 None 时表示无 GUI 模式"""
|
||||
global server_pid
|
||||
|
||||
if NAS_MODE:
|
||||
run_steps_nas(root, start_time)
|
||||
return
|
||||
|
||||
def elapsed():
|
||||
return int(time.time() - start_time)
|
||||
|
||||
def upd(pct, step, msg):
|
||||
full = f"{msg} (已用时 {elapsed()} 秒)"
|
||||
set_progress(pct, step, full)
|
||||
if not USE_GUI:
|
||||
notify(f"{pct}% | {step}: {msg}")
|
||||
|
||||
try:
|
||||
os.chdir(PROJECT)
|
||||
except Exception as e:
|
||||
upd(0, "错误", str(e))
|
||||
return
|
||||
|
||||
# 1. Ollama 本地大模型 (0-15%)
|
||||
upd(0, "本地大模型", "检查 Ollama (localhost:11434)...")
|
||||
if not http_ok("http://localhost:11434/api/tags"):
|
||||
upd(2, "本地大模型", "正在启动 Ollama...")
|
||||
subprocess.run(["open", "-a", "Ollama"], capture_output=True, timeout=5)
|
||||
for i in range(45):
|
||||
if http_ok("http://localhost:11434/api/tags"):
|
||||
break
|
||||
upd(2 + i // 3, "本地大模型", f"等待 Ollama 就绪... ({i+1}s)")
|
||||
time.sleep(1)
|
||||
upd(15, "本地大模型", "Ollama 已就绪")
|
||||
|
||||
# 2. Docker (15-35%)
|
||||
upd(15, "Docker", "检查 Docker Engine...")
|
||||
subprocess.run(["open", "-a", "Docker"], capture_output=True, timeout=5)
|
||||
for i in range(60):
|
||||
r = subprocess.run(["docker", "info"], capture_output=True, timeout=5)
|
||||
if r.returncode == 0:
|
||||
break
|
||||
upd(15 + i // 3, "Docker", f"等待 Docker Engine... ({i*2}s)")
|
||||
time.sleep(2)
|
||||
upd(35, "Docker", "Docker 已就绪")
|
||||
|
||||
# 3. MongoDB (35-45%)
|
||||
upd(35, "MongoDB", "启动 MongoDB 容器...")
|
||||
subprocess.run(["docker", "start", "datacenter_mongodb"], capture_output=True, timeout=10)
|
||||
time.sleep(3)
|
||||
upd(45, "MongoDB", "MongoDB 已就绪")
|
||||
|
||||
# 4. 若 3117 已监听,直接完成
|
||||
if port_listen(3117):
|
||||
upd(95, "神射手", "服务已在运行")
|
||||
time.sleep(1)
|
||||
subprocess.run(["open", "http://localhost:3117"], timeout=3)
|
||||
upd(100, "完成", "已打开浏览器")
|
||||
return
|
||||
|
||||
# 5. 启动 pnpm dev (45-90%)
|
||||
upd(45, "神射手", "启动开发服务 (pnpm dev)...")
|
||||
proc = subprocess.Popen(
|
||||
["pnpm", "dev"],
|
||||
cwd=PROJECT,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
server_pid = proc.pid
|
||||
|
||||
# 6. 等待 3117 (90-100%)
|
||||
for i in range(90):
|
||||
if port_listen(3117):
|
||||
break
|
||||
pct = 45 + min(44, i * 2)
|
||||
upd(pct, "神射手", f"等待端口 3117 就绪... ({i+1}s)")
|
||||
time.sleep(1)
|
||||
|
||||
upd(95, "神射手", "正在打开浏览器...")
|
||||
subprocess.run(["open", "http://localhost:3117"], timeout=3)
|
||||
upd(100, "完成", "神射手已启动")
|
||||
|
||||
# 有 GUI 时保持进程直到窗口关闭;无 GUI 时直接退出(pnpm 在后台继续运行)
|
||||
if root is not None:
|
||||
try:
|
||||
proc.wait()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
global server_pid
|
||||
|
||||
if not USE_GUI:
|
||||
run_steps(None, time.time())
|
||||
return
|
||||
|
||||
try:
|
||||
root = tk.Tk()
|
||||
root.title("神射手 (NAS) - 启动中" if NAS_MODE else "神射手 - 启动中")
|
||||
root.geometry("420x180")
|
||||
root.resizable(False, False)
|
||||
|
||||
f = ttk.Frame(root, padding=16)
|
||||
f.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
title = "神射手 (NAS) 数据中台" if NAS_MODE else "神射手 数据中台"
|
||||
subtitle = "NAS 192.168.1.201:3117" if NAS_MODE else "Ollama → Docker → MongoDB → 神射手"
|
||||
ttk.Label(f, text=title, font=("", 14, "bold")).pack(anchor=tk.W)
|
||||
ttk.Label(f, text=subtitle, font=("", 9)).pack(anchor=tk.W)
|
||||
|
||||
f2 = ttk.Frame(f)
|
||||
f2.pack(fill=tk.X, pady=(12, 0))
|
||||
|
||||
lb_pct = ttk.Label(f2, text="0%", font=("", 11, "bold"))
|
||||
lb_pct.pack(side=tk.RIGHT)
|
||||
lb_step = ttk.Label(f2, text="初始化", font=("", 10))
|
||||
lb_step.pack(anchor=tk.W)
|
||||
lb_msg = ttk.Label(f2, text="准备启动...", font=("", 9))
|
||||
lb_msg.pack(anchor=tk.W)
|
||||
p = ttk.Progressbar(f2, length=380, mode="determinate")
|
||||
p.pack(fill=tk.X, pady=(4, 0))
|
||||
p["value"] = 0
|
||||
|
||||
_widgets["root"] = root
|
||||
_widgets["pct"] = lb_pct
|
||||
_widgets["step"] = lb_step
|
||||
_widgets["msg"] = lb_msg
|
||||
_widgets["progress"] = p
|
||||
|
||||
def on_close():
|
||||
global server_pid
|
||||
if server_pid and not NAS_MODE:
|
||||
try:
|
||||
os.kill(server_pid, signal.SIGTERM)
|
||||
except Exception:
|
||||
pass
|
||||
root.destroy()
|
||||
|
||||
root.protocol("WM_DELETE_WINDOW", on_close)
|
||||
root.update_idletasks()
|
||||
except Exception:
|
||||
run_steps(None, time.time())
|
||||
return
|
||||
|
||||
t = threading.Thread(target=lambda: run_steps(root, time.time()))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
try:
|
||||
root.mainloop()
|
||||
except Exception:
|
||||
pass
|
||||
if server_pid and t.is_alive():
|
||||
try:
|
||||
os.kill(server_pid, signal.SIGTERM)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
165
scripts/screenshot-pages.mjs
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 神射手前端全站截图脚本
|
||||
* 使用中文文件名:{序号}-{页面中文名}-视口.png / {序号}-{页面中文名}-长图.png
|
||||
* 使用前请先启动:pnpm dev(端口 3117)
|
||||
* 运行:node scripts/screenshot-pages.mjs 或 pnpm exec node scripts/screenshot-pages.mjs
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..");
|
||||
const BASE_URL = process.env.BASE_URL || "http://localhost:3117";
|
||||
const OUT_DIR = process.env.OUT_DIR || join(ROOT, "开发文档", "4、前端", "截图");
|
||||
const VIEWPORT = { width: 1280, height: 800 };
|
||||
const WAIT_MS = 2500;
|
||||
const NAV_TIMEOUT = 15000;
|
||||
|
||||
// 路由与中文名称(序号从 06 起,01-05 已存在)
|
||||
const ROUTES = [
|
||||
{ path: "/", name: "首页" },
|
||||
{ path: "/ai-analysis", name: "AI分析" },
|
||||
{ path: "/data-market", name: "数据市场" },
|
||||
{ path: "/tag-portrait", name: "标签画像" },
|
||||
{ path: "/value-model", name: "估值模型" },
|
||||
{ path: "/settings", name: "设置" },
|
||||
{ path: "/documentation", name: "文档" },
|
||||
{ path: "/login", name: "登录" },
|
||||
{ path: "/traffic-pool", name: "流量池" },
|
||||
{ path: "/user-portrait", name: "用户画像" },
|
||||
{ path: "/user-portrait/tags", name: "用户画像-标签" },
|
||||
{ path: "/user-portrait/1", name: "用户画像详情" },
|
||||
{ path: "/tag-portrait/tags", name: "标签画像-标签" },
|
||||
{ path: "/tag-portrait/portrait", name: "标签画像-画像" },
|
||||
{ path: "/tag-portrait/crowd", name: "标签画像-人群" },
|
||||
{ path: "/data-asset", name: "数据资产" },
|
||||
{ path: "/data-asset/packages", name: "数据资产-包" },
|
||||
{ path: "/data-asset/api-market", name: "数据资产-API市场" },
|
||||
{ path: "/data-market/packages", name: "数据市场-包" },
|
||||
{ path: "/data-market/api", name: "数据市场-API" },
|
||||
{ path: "/data-market/open-api", name: "数据市场-开放API" },
|
||||
{ path: "/value-model/models", name: "模型列表" },
|
||||
{ path: "/value-model/assessment", name: "价值评估" },
|
||||
{ path: "/value-model/reports", name: "估值报表" },
|
||||
{ path: "/value-assessment", name: "价值评估页" },
|
||||
{ path: "/user-valuation", name: "用户估值" },
|
||||
{ path: "/user-value", name: "用户价值" },
|
||||
{ path: "/data-integration", name: "数据集成" },
|
||||
{ path: "/data-ingestion", name: "数据接入" },
|
||||
{ path: "/data-ingestion/sources", name: "数据接入-数据源" },
|
||||
{ path: "/data-ingestion/tasks", name: "数据接入-任务" },
|
||||
{ path: "/data-ingestion/lineage", name: "数据接入-血缘" },
|
||||
{ path: "/data-ingestion/cleaning", name: "数据接入-清洗" },
|
||||
{ path: "/data-ingestion/ai-engine", name: "数据接入-AI引擎" },
|
||||
{ path: "/data-governance", name: "数据治理" },
|
||||
{ path: "/data-governance/tasks", name: "数据治理-任务" },
|
||||
{ path: "/data-governance/quality", name: "数据治理-质量" },
|
||||
{ path: "/data-governance/sources", name: "数据治理-数据源" },
|
||||
{ path: "/data-governance/cleaning", name: "数据治理-清洗" },
|
||||
{ path: "/platform/dashboard", name: "平台-仪表盘" },
|
||||
{ path: "/platform/data-management", name: "平台-数据管理" },
|
||||
{ path: "/platform/user-portrait", name: "平台-用户画像" },
|
||||
{ path: "/platform/value-assessment", name: "平台-价值评估" },
|
||||
{ path: "/platform/ai-assistant", name: "平台-AI助手" },
|
||||
{ path: "/workspace/auto-group", name: "工作台-自动分组" },
|
||||
{ path: "/workspace/moments-sync", name: "工作台-朋友圈同步" },
|
||||
{ path: "/workspace/moments-sync/new", name: "工作台-朋友圈同步-新建" },
|
||||
{ path: "/workspace/pricing", name: "工作台-定价" },
|
||||
{ path: "/workspace/pricing/new", name: "工作台-定价-新建" },
|
||||
{ path: "/content", name: "内容" },
|
||||
{ path: "/content/new", name: "内容-新建" },
|
||||
{ path: "/scenarios", name: "场景" },
|
||||
{ path: "/scenarios/phone", name: "场景-手机" },
|
||||
{ path: "/scenarios/api", name: "场景-API" },
|
||||
{ path: "/system/health", name: "系统-健康" },
|
||||
{ path: "/system/metrics", name: "系统-指标" },
|
||||
{ path: "/system/alerts", name: "系统-告警" },
|
||||
{ path: "/system/logs", name: "系统-日志" },
|
||||
{ path: "/monitoring", name: "监控" },
|
||||
{ path: "/monitoring/health", name: "监控-健康" },
|
||||
{ path: "/monitoring/business", name: "监控-业务" },
|
||||
{ path: "/monitoring/alerts", name: "监控-告警" },
|
||||
{ path: "/ai-assistant", name: "AI助手" },
|
||||
{ path: "/ai-agent", name: "AI智能体" },
|
||||
{ path: "/ai-agent/chat", name: "AI智能体-对话" },
|
||||
{ path: "/ai-agent/smart-tag", name: "AI智能体-智能打标" },
|
||||
{ path: "/ai-agent/nlq", name: "AI智能体-NLQ" },
|
||||
{ path: "/ai-agent/report", name: "AI智能体-报告" },
|
||||
{ path: "/ai-insight", name: "AI洞察" },
|
||||
{ path: "/wechat-accounts", name: "企微账号" },
|
||||
{ path: "/data-output/subscription", name: "数据输出-订阅" },
|
||||
{ path: "/data-output/packages", name: "数据输出-包" },
|
||||
{ path: "/data-output/api-market", name: "数据输出-API市场" },
|
||||
{ path: "/overview/dashboard", name: "概览-仪表盘" },
|
||||
{ path: "/overview/search", name: "概览-搜索" },
|
||||
{ path: "/overview/monitoring", name: "概览-监控" },
|
||||
{ path: "/intelligent-search", name: "智能搜索" },
|
||||
{ path: "/database-structure", name: "数据库结构" },
|
||||
{ path: "/data-dictionary", name: "数据字典" },
|
||||
{ path: "/data-platform", name: "数据平台" },
|
||||
{ path: "/data-middle-platform", name: "数据中台" },
|
||||
{ path: "/api-interface", name: "API接口" },
|
||||
{ path: "/user-discovery", name: "用户发现" },
|
||||
{ path: "/user-profile", name: "用户资料" },
|
||||
{ path: "/rfm", name: "RFM" },
|
||||
{ path: "/group-sync", name: "群同步" },
|
||||
{ path: "/conversion", name: "转化" },
|
||||
{ path: "/devices", name: "设备" },
|
||||
];
|
||||
|
||||
function safeFilename(name) {
|
||||
return name.replace(/[/\\?*:|"]/g, "-").trim();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
|
||||
console.log("输出目录:", OUT_DIR);
|
||||
console.log("基础URL:", BASE_URL);
|
||||
console.log("共", ROUTES.length, "个页面\n");
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: VIEWPORT,
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.setDefaultNavigationTimeout(NAV_TIMEOUT);
|
||||
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
for (let i = 0; i < ROUTES.length; i++) {
|
||||
const { path, name } = ROUTES[i];
|
||||
const num = String(i + 1).padStart(2, "0");
|
||||
const safeName = safeFilename(name);
|
||||
const url = BASE_URL + path;
|
||||
const viewportPath = join(OUT_DIR, `${num}-${safeName}-视口.png`);
|
||||
const fullPath = join(OUT_DIR, `${num}-${safeName}-长图.png`);
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: NAV_TIMEOUT });
|
||||
await page.waitForTimeout(WAIT_MS);
|
||||
await page.screenshot({ path: viewportPath, type: "png" });
|
||||
await page.screenshot({ path: fullPath, type: "png", fullPage: true });
|
||||
console.log(`[${num}] ${name} 视口+长图 OK`);
|
||||
ok++;
|
||||
} catch (e) {
|
||||
console.error(`[${num}] ${name} 失败:`, e.message);
|
||||
try {
|
||||
await page.screenshot({ path: fullPath, type: "png", fullPage: true }).catch(() => {});
|
||||
console.log(`[${num}] ${name} 已保存长图`);
|
||||
} catch (_) {}
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log("\n完成. 成功:", ok, "失败:", fail);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
38
启动website.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# 一键启动 website(玩值电竞 + 神射手),访问 http://localhost:3001
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "========== 检查 Docker =========="
|
||||
if docker info >/dev/null 2>&1; then
|
||||
echo "Docker 已就绪,正在启动 website..."
|
||||
docker compose up -d --build
|
||||
echo ""
|
||||
echo "========== 启动完成 =========="
|
||||
echo "玩值电竞:http://localhost:3001"
|
||||
echo "神射手: http://localhost:3117"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Docker 未运行,正在打开 Docker Desktop..."
|
||||
open -a "Docker Desktop" 2>/dev/null || true
|
||||
echo "等待 Docker 就绪(最多约 3 分钟)..."
|
||||
for i in $(seq 1 36); do
|
||||
if docker info >/dev/null 2>&1; then
|
||||
echo "Docker 已就绪,正在启动 website..."
|
||||
docker compose up -d --build
|
||||
echo ""
|
||||
echo "========== 启动完成 =========="
|
||||
echo "玩值电竞:http://localhost:3001"
|
||||
echo "神射手: http://localhost:3117"
|
||||
exit 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Docker 在 3 分钟内未就绪。请:"
|
||||
echo "1. 手动打开 Docker Desktop,等待完全启动(托盘图标就绪)"
|
||||
echo "2. 再在本目录执行:docker compose up -d --build"
|
||||
echo "3. 浏览器访问 http://localhost:3001"
|
||||
exit 1
|
||||
146
开发文档/00_十目录索引.md
@@ -1,146 +0,0 @@
|
||||
# 神射手数据中台 - 十目录统一索引
|
||||
|
||||
> 📅 更新: 2026-01-31
|
||||
> 🎯 基于 dev-template-project-manager skill 规范
|
||||
> 📋 各板块子文档统一维护
|
||||
|
||||
---
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
开发文档/
|
||||
├── 00_十目录索引.md ← 本文档(统一入口)
|
||||
├── 00_项目核心文档.md # 项目总览
|
||||
├── 01_开发进度文档.md # 进度跟踪
|
||||
├── 02_提示词文档.md # 提示词存档
|
||||
│
|
||||
├── 1、需求/ # CFO + 产品负责人
|
||||
│ ├── _智能展开.md # 需求引擎激活
|
||||
│ ├── 业务需求.md # 业务需求文档
|
||||
│ ├── 核心需求提取.md # 从项目提取的核心需求 ← 新增
|
||||
│ └── 成本.md # 成本估算
|
||||
│
|
||||
├── 2、架构/ # CTO + 架构师
|
||||
│ ├── _智能展开.md # 架构引擎激活
|
||||
│ ├── 系统架构.md # 架构图
|
||||
│ ├── 核心架构逻辑.md # 从项目提取的架构逻辑 ← 新增
|
||||
│ └── 技术选型.md # 技术栈
|
||||
│
|
||||
├── 3、原型/ # UI/UX 设计师
|
||||
│ ├── _智能展开.md # 原型引擎激活
|
||||
│ ├── 页面结构.md # 从项目提取的页面结构 ← 新增
|
||||
│ └── 原型设计规范.md # 设计规范
|
||||
│
|
||||
├── 4、前端/ # 前端主程
|
||||
│ ├── _智能展开.md # 前端引擎激活
|
||||
│ ├── 核心组件代码.md # 从项目提取的核心代码 ← 新增
|
||||
│ └── 前端开发规范.md # 开发规范
|
||||
│
|
||||
├── 5、接口/ # API 架构师
|
||||
│ ├── _智能展开.md # 接口引擎激活
|
||||
│ ├── API清单与核心逻辑.md # 从项目提取的API逻辑 ← 新增
|
||||
│ └── 接口定义规范.md # 接口规范
|
||||
│
|
||||
├── 6、后端/ # Python 架构师
|
||||
│ ├── _智能展开.md # 后端引擎激活
|
||||
│ ├── MongoDB连接器核心.md # 从项目提取的lib/mongodb逻辑 ← 新增
|
||||
│ └── 后端开发规范.md # 开发规范
|
||||
│
|
||||
├── 7、数据库/ # DBA
|
||||
│ ├── _智能展开.md # 数据库引擎激活
|
||||
│ ├── ER与查询逻辑.md # 从项目提取的集合与查询 ← 新增
|
||||
│ └── 数据库管理规范.md # 管理规范
|
||||
│
|
||||
├── 8、部署/ # DevOps
|
||||
│ ├── _智能展开.md # 部署引擎激活
|
||||
│ ├── 启动与部署脚本.md # 从项目提取的部署逻辑 ← 新增
|
||||
│ └── 自动化部署流程.md # 部署流程
|
||||
│
|
||||
├── 9、手册/ # 技术文档专家
|
||||
│ ├── _智能展开.md # 手册引擎激活
|
||||
│ ├── 快速使用手册.md # 从项目提取的使用说明 ← 新增
|
||||
│ └── 说明手册提示词.md # 手册模板
|
||||
│
|
||||
└── 10、项目管理/ # 高级 PM
|
||||
├── _智能展开.md # 管理引擎激活
|
||||
├── 执行表.md # 任务清单(链接.apm)
|
||||
└── 项目管理提示词.md # 管理模板
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 跨目录联动
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[1、需求] --> B[2、架构]
|
||||
A --> C[3、原型]
|
||||
B --> D[5、接口]
|
||||
B --> E[7、数据库]
|
||||
D --> F[4、前端]
|
||||
D --> G[6、后端]
|
||||
E --> G
|
||||
F --> H[8、部署]
|
||||
G --> H
|
||||
H --> I[9、手册]
|
||||
A --> J[10、项目管理]
|
||||
B --> J
|
||||
F --> J
|
||||
G --> J
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 快速指令
|
||||
|
||||
| 目录 | 激活指令 | 输出 |
|
||||
|:---:|:---|:---|
|
||||
| 1 | `@需求引擎 展开 [需求描述]` | 业务需求文档 |
|
||||
| 2 | `@架构引擎 展开 [模块名]` | 系统架构图 |
|
||||
| 3 | `@原型引擎 展开 [页面名]` | 页面结构 |
|
||||
| 4 | `@前端引擎 展开 [组件名]` | React 组件代码 |
|
||||
| 5 | `@接口引擎 展开 [API名]` | API 文档 |
|
||||
| 6 | `@后端引擎 展开 [服务名]` | 后端代码 |
|
||||
| 7 | `@数据库引擎 展开 [集合名]` | ER 图 |
|
||||
| 8 | `@部署引擎 展开` | 部署脚本 |
|
||||
| 9 | `@手册引擎 展开 [模块名]` | 用户手册 |
|
||||
| 10 | `@项目管理 查看进度` | 执行表 |
|
||||
|
||||
---
|
||||
|
||||
## 📂 核心代码提取清单
|
||||
|
||||
| 目录 | 提取文件 | 核心逻辑 |
|
||||
|:---|:---|:---|
|
||||
| 1-需求 | PRD/对话记录 | 5大模块、用户资产数字化 |
|
||||
| 2-架构 | .apm/docs/architecture.md | 四层架构、数据流向 |
|
||||
| 3-原型 | app/ 路由结构 | 5模块+子页面 |
|
||||
| 4-前端 | app/page.tsx, components/ | AI对话、卡片、布局 |
|
||||
| 5-接口 | app/api/*/route.ts | 25个API、意图解析 |
|
||||
| 6-后端 | lib/mongodb.ts | 连接池、查询、归一化 |
|
||||
| 7-数据库 | KR.用户估值等 | 集合结构、字段映射 |
|
||||
| 8-部署 | package.json, next.config | npm脚本、端口 |
|
||||
| 9-手册 | SKILL.md | 快速开始、查询示例 |
|
||||
| 10-管理 | .apm/execution-table.md | 31任务、里程碑 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 十、运行与端口
|
||||
|
||||
```bash
|
||||
# 启动神射手
|
||||
cd 神射手 && npm run dev -- -p 3001
|
||||
|
||||
# 端口说明
|
||||
# 3001: 神射手前端(保留)
|
||||
# 27017: MongoDB(保留)
|
||||
# 8000: 卡若AI网关(保留)
|
||||
# 3000、3002: 已关闭
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*版本: v1.1 | 更新: 2026-01-31 | 维护: 卡若AI*
|
||||
@@ -1,269 +0,0 @@
|
||||
# 神射手数据中台 - 项目核心文档
|
||||
|
||||
> 📅 最后更新:2026-01-31 18:30
|
||||
> 🎯 项目阶段:P3 执行
|
||||
> 🟢 健康状态:正常
|
||||
|
||||
---
|
||||
|
||||
## 📊 项目概览
|
||||
|
||||
| 项目 | 信息 |
|
||||
|:---|:---|
|
||||
| 项目名称 | 神射手数据中台 |
|
||||
| 项目描述 | 用户资产数字化中台,整合20亿+用户数据的查询、标签、画像和流量池管理 |
|
||||
| 开始日期 | 2026-01-29 |
|
||||
| 目标日期 | 2026-02-07 |
|
||||
| 当前阶段 | P3 执行 |
|
||||
| 负责人 | 卡若AI |
|
||||
| 前端路径 | `/Users/karuo/Documents/开发/2、私域银行/神射手` |
|
||||
| 后端Skill | `/Users/karuo/Documents/个人/卡若AI/04_卡火(火)/_团队成员/火炬/神射手/SKILL.md` |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术栈
|
||||
|
||||
```yaml
|
||||
前端:
|
||||
框架: Next.js 14
|
||||
UI库: Radix UI + TailwindCSS
|
||||
图表: Recharts
|
||||
状态: React Hooks (useState, useEffect)
|
||||
风格: 苹果毛玻璃 (backdrop-blur + bg-white/80)
|
||||
|
||||
后端:
|
||||
数据库: MongoDB (Docker localhost:27017)
|
||||
认证: admin / admin123
|
||||
API: Next.js API Routes (25个端点)
|
||||
网关: 卡若AI FastAPI Gateway
|
||||
|
||||
数据规模:
|
||||
总数据库: 26个KR_*库
|
||||
总用户数: 20.13亿条
|
||||
总容量: 222GB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
神射手/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── page.tsx # 首页 - AI对话
|
||||
│ ├── data-ingestion/ # 数据接入模块
|
||||
│ │ ├── sources/ # 数据源管理
|
||||
│ │ ├── ai-engine/ # AI标签引擎
|
||||
│ │ ├── cleaning/ # 清洗规则
|
||||
│ │ ├── tasks/ # 任务调度
|
||||
│ │ └── lineage/ # 数据血缘
|
||||
│ ├── tag-portrait/ # 标签画像模块
|
||||
│ │ ├── tags/ # 标签管理
|
||||
│ │ ├── portrait/ # 用户画像
|
||||
│ │ └── crowd/ # 人群圈选
|
||||
│ ├── ai-agent/ # AI Agent模块
|
||||
│ │ ├── channels/ # 渠道配置
|
||||
│ │ ├── smart-tag/ # AI打标
|
||||
│ │ ├── data-cleaning/ # AI清洗
|
||||
│ │ └── report/ # 智能报告
|
||||
│ ├── data-market/ # 数据市场模块
|
||||
│ │ ├── packages/ # 流量包管理
|
||||
│ │ └── api/ # API服务
|
||||
│ ├── monitoring/ # 系统监控模块
|
||||
│ └── api/ # API路由 (25个)
|
||||
├── lib/
|
||||
│ └── mongodb.ts # MongoDB连接器
|
||||
├── components/ # 公共组件
|
||||
├── .apm/ # 项目管理工作区
|
||||
│ ├── project-state.md # 项目状态
|
||||
│ ├── execution-table.md # 执行表
|
||||
│ ├── conversation-log.md # 对话记录
|
||||
│ └── docs/architecture.md # 架构图
|
||||
└── 开发文档/ # 开发文档
|
||||
├── 00_项目核心文档.md # 本文档
|
||||
├── 01_开发进度文档.md # 进度跟踪
|
||||
└── 02_提示词文档.md # 提示词存档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 API清单 (25个端点)
|
||||
|
||||
### 核心API
|
||||
|
||||
| 路由 | 方法 | 功能 | 状态 |
|
||||
|:-----|:-----|:-----|:----:|
|
||||
| `/api/ai-chat` | GET/POST | AI智能对话、系统状态 | ✅ |
|
||||
| `/api/data-sources` | GET/POST | 数据源管理、连接测试 | ✅ |
|
||||
| `/api/tags` | GET/POST | 标签统计、标签创建 | ✅ |
|
||||
| `/api/portrait` | GET/POST | 用户画像、人群分布 | ✅ |
|
||||
| `/api/traffic-packages` | GET/POST | 流量池、流量包导出 | ✅ |
|
||||
| `/api/crowd-pools` | GET | 人群圈选按项目 | ✅ |
|
||||
| `/api/database-structure` | GET | 数据库结构、血缘节点 | ✅ |
|
||||
| `/api/monitoring` | GET | 系统监控、健康检查 | ✅ |
|
||||
| `/api/channels` | GET/POST | 渠道对接、飞书集成 | ✅ |
|
||||
|
||||
### 辅助API
|
||||
|
||||
| 路由 | 方法 | 功能 |
|
||||
|:-----|:-----|:-----|
|
||||
| `/api/users` | GET | 用户列表查询 |
|
||||
| `/api/search` | POST | 多条件搜索 |
|
||||
| `/api/ai-tagging` | GET/POST | AI打标引擎 |
|
||||
| `/api/cleaning-rules` | GET/POST | 清洗规则管理 |
|
||||
| `/api/system-status` | GET | 系统状态 |
|
||||
| `/api/rfm/*` | GET/POST | RFM分析系列 |
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 数据库结构
|
||||
|
||||
### 核心集合
|
||||
|
||||
| 集合 | 数据库 | 文档数 | 用途 |
|
||||
|:-----|:-------|-------:|:-----|
|
||||
| 用户估值 | KR | 1436万 | 统一画像、RFM评分 |
|
||||
| QQ+手机 | KR_腾讯 | 7.05亿 | QQ↔手机关联 |
|
||||
| 微博uid+手机 | KR_微博 | 2.17亿 | 微博UID↔手机 |
|
||||
| 用户资产统一视图 | KR_存客宝 | 21.6万 | 存客宝用户 |
|
||||
| 用户资产统一视图 | KR_点了码 | 1000 | 点了码用户 |
|
||||
|
||||
### 字段映射
|
||||
|
||||
```yaml
|
||||
KR.用户估值:
|
||||
phone: '+8613407000001' # 手机号
|
||||
phone_masked: '134****0001' # 脱敏手机
|
||||
name: '姓名' # 姓名
|
||||
user_evaluation_score: 2227 # 估值分 (0-5000+)
|
||||
user_level: 'A' # 用户等级
|
||||
province: '广东' # 省份
|
||||
city: '深圳' # 城市
|
||||
source_channels: ['KR_手机'] # 数据来源
|
||||
unified_tags: ['高价值', '活跃'] # 统一标签
|
||||
traffic_pool: {pool_name: '黄金池'} # 流量池
|
||||
|
||||
KR_腾讯.QQ+手机:
|
||||
qq: '3520685418' # QQ号
|
||||
phone: '18879944144' # 手机号
|
||||
QQ号评分: 100 # QQ评分
|
||||
手机号评分: 470 # 手机评分
|
||||
省份: '广西' # 省份
|
||||
运营商: '移动' # 运营商
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ AI标签体系
|
||||
|
||||
### 标签分类
|
||||
|
||||
| 分类 | 标签示例 | 生成方式 |
|
||||
|:-----|:---------|:---------|
|
||||
| 价值标签 | S/A/B/C/D级用户 | RFM自动计算 |
|
||||
| 行为标签 | 高频活跃、流失风险 | 行为分析 |
|
||||
| 渠道标签 | 微信渠道、抖音渠道 | 数据来源 |
|
||||
| 地域标签 | 一线城市、厦门本地 | 地址解析 |
|
||||
|
||||
### RFM评分规则
|
||||
|
||||
```
|
||||
综合评分 = R×0.3 + F×0.3 + M×0.4
|
||||
|
||||
等级划分 (基于 user_evaluation_score):
|
||||
├── 钻石池: ≥3000分
|
||||
├── 黄金池: 2000-2999分
|
||||
├── 白银池: 1000-1999分
|
||||
├── 青铜池: 500-999分
|
||||
└── 潜力池: <500分
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 外部集成
|
||||
|
||||
### 飞书机器人
|
||||
|
||||
```yaml
|
||||
网关路径: /Users/karuo/Documents/个人/卡若AI/_共享模块/deploy/gateway/routers/feishu.py
|
||||
|
||||
API端点:
|
||||
- GET /feishu/test # 连接测试
|
||||
- GET /feishu/chats # 获取群列表
|
||||
- POST /feishu/send_message # 发送消息
|
||||
- POST /feishu/send_minutes # 发送会议纪要
|
||||
- POST /feishu/webhook # 事件回调
|
||||
|
||||
环境变量:
|
||||
FEISHU_APP_ID: cli_xxxxx
|
||||
FEISHU_APP_SECRET: xxxxx
|
||||
FEISHU_VERIFICATION_TOKEN: xxxxx
|
||||
```
|
||||
|
||||
### 企业微信
|
||||
|
||||
```yaml
|
||||
网关路径: gateway/routers/wecom.py
|
||||
|
||||
API端点:
|
||||
- POST /wecom/callback # 消息回调
|
||||
|
||||
环境变量:
|
||||
WECOM_CORP_ID: wwxxxxx
|
||||
WECOM_AGENT_ID: 1000002
|
||||
WECOM_SECRET: xxxxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速启动
|
||||
|
||||
```bash
|
||||
# 1. 确认MongoDB运行
|
||||
docker ps | grep mongo
|
||||
|
||||
# 2. 启动开发服务器(端口 3117)
|
||||
cd /Users/karuo/Documents/开发/2、私域银行/神射手
|
||||
pnpm dev # 或 ./scripts/start.sh --kill(带端口冲突检查)
|
||||
|
||||
# 3. 访问
|
||||
http://localhost:3117
|
||||
|
||||
# 4. 测试API
|
||||
curl http://localhost:3117/api/ai-chat
|
||||
curl http://localhost:3117/api/monitoring?action=health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
| 文档 | 路径 | 用途 |
|
||||
|:-----|:-----|:-----|
|
||||
| **十目录索引** | `00_十目录索引.md` | 统一入口、跨目录联动 |
|
||||
| 开发进度 | `01_开发进度文档.md` | 进度跟踪、任务管理 |
|
||||
| 提示词存档 | `02_提示词文档.md` | 对话历史、提示词模板 |
|
||||
| 项目状态 | `.apm/project-state.md` | 项目总览 |
|
||||
| 执行表 | `.apm/execution-table.md` | 任务清单 |
|
||||
| 架构图 | `.apm/docs/architecture.md` | 系统架构 |
|
||||
| 后端Skill | `卡若AI/04_卡火/火炬/神射手/SKILL.md` | 后端能力 |
|
||||
| Cursor规则 | `.cursor/rules/shensheshou.mdc` | 对话规范 |
|
||||
|
||||
### 十目录子文档(按 skill 规范)
|
||||
|
||||
| 目录 | 核心子文档 | 内容 |
|
||||
|:---|:---|:---|
|
||||
| 1-需求 | 核心需求提取.md | 用户故事、MVP边界 |
|
||||
| 2-架构 | 核心架构逻辑.md | 四层架构、数据流向 |
|
||||
| 3-原型 | 页面结构.md | 路由树、组件结构 |
|
||||
| 4-前端 | 核心组件代码.md | AI对话、毛玻璃样式 |
|
||||
| 5-接口 | API清单与核心逻辑.md | 25API、意图解析 |
|
||||
| 6-后端 | MongoDB连接器核心.md | 连接池、跨库查询 |
|
||||
| 7-数据库 | ER与查询逻辑.md | 集合结构、分桶逻辑 |
|
||||
| 8-部署 | 启动与部署脚本.md | npm脚本、端口 |
|
||||
| 9-手册 | 快速使用手册.md | 查询示例、FAQ |
|
||||
| 10-管理 | 执行表.md | 任务映射、联动 |
|
||||
|
||||
---
|
||||
|
||||
*版本: v1.4.0 | 更新: 2026-01-31 | 维护: 卡若AI*
|
||||
@@ -1,190 +0,0 @@
|
||||
# 神射手数据中台 - 开发进度
|
||||
|
||||
> 📅 最后更新:2026-01-31 18:30
|
||||
> 🎯 项目阶段:P3 执行
|
||||
> 🟢 健康状态:正常
|
||||
|
||||
---
|
||||
|
||||
## 📈 进度摘要
|
||||
|
||||
| 指标 | 数值 |
|
||||
|:---|:---|
|
||||
| 总任务数 | 30 |
|
||||
| 已完成 | 28 (93%) |
|
||||
| 进行中 | 1 |
|
||||
| 待开始 | 1 |
|
||||
| 阻塞中 | 0 |
|
||||
|
||||
### 进度条
|
||||
|
||||
`[██████████████████░░] 93%`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 当前里程碑
|
||||
|
||||
| 里程碑 | 目标日期 | 状态 | 交付物 |
|
||||
|:---|:---|:---:|:---|
|
||||
| M1 基础框架 | 01-29 | ✅ | Next.js项目+MongoDB连接 |
|
||||
| M2 核心功能 | 01-30 | ✅ | 5大模块前端页面 |
|
||||
| M3 真实数据 | 01-31 | ✅ | API对接真实数据库 |
|
||||
| M4 外部集成 | 02-03 | 🔄 | 飞书/企微对接 |
|
||||
| M5 正式上线 | 02-07 | ⏳ | 生产环境部署 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 当前风险/阻碍
|
||||
|
||||
| 问题 | 严重度 | 状态 | 处理方案 |
|
||||
|:---|:---:|:---:|:---|
|
||||
| 飞书机器人未配置 | 🟡 | 监控中 | 需配置环境变量 |
|
||||
| API响应慢(大表) | 🟢 | 已缓解 | 使用$sample采样 |
|
||||
|
||||
---
|
||||
|
||||
## 状态说明
|
||||
|
||||
| 状态 | 含义 |
|
||||
|:---:|:---|
|
||||
| ✅ | Done - 已完成 |
|
||||
| 🔄 | In Progress - 进行中 |
|
||||
| ⏳ | Pending - 待开始 |
|
||||
| ❌ | Blocked - 阻塞中 |
|
||||
|
||||
---
|
||||
|
||||
## P1 启动阶段
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T001 | 项目初始化 | 创建Next.js项目 | ✅ | 项目结构 |
|
||||
| T002 | 数据库连接 | MongoDB连接器 | ✅ | lib/mongodb.ts |
|
||||
| T003 | AI对话API | 智能查询接口 | ✅ | /api/ai-chat |
|
||||
|
||||
---
|
||||
|
||||
## P2 开发阶段 - 数据概览
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T004 | 首页 | AI对话+系统状态 | ✅ | app/page.tsx |
|
||||
|
||||
---
|
||||
|
||||
## P2 开发阶段 - 数据接入
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T005 | 数据源管理 | 数据库列表+连接 | ✅ | /data-ingestion/sources |
|
||||
| T006 | AI标签引擎 | 动态任务列表 | ✅ | /data-ingestion/ai-engine |
|
||||
| T007 | 清洗规则 | 7条默认规则 | ✅ | /data-ingestion/cleaning |
|
||||
| T008 | 任务调度 | 调度管理页 | ✅ | /data-ingestion/tasks |
|
||||
| T009 | 数据血缘 | 动态节点API | ✅ | /data-ingestion/lineage |
|
||||
|
||||
---
|
||||
|
||||
## P2 开发阶段 - 标签画像
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T010 | 标签管理 | 真实标签API | ✅ | /tag-portrait/tags |
|
||||
| T011 | 用户画像 | 流量池统计 | ✅ | /tag-portrait/portrait |
|
||||
| T012 | 人群圈选 | 按项目分类 | ✅ | /tag-portrait/crowd |
|
||||
|
||||
---
|
||||
|
||||
## P2 开发阶段 - AI Agent
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T013 | 渠道配置 | 飞书/企微/微信 | ✅ | /ai-agent/channels |
|
||||
| T014 | AI打标 | 提示词编辑 | ✅ | /ai-agent/smart-tag |
|
||||
| T015 | AI清洗 | 规则配置 | ✅ | /ai-agent/data-cleaning |
|
||||
| T016 | 智能报告 | 模板管理 | ✅ | /ai-agent/report |
|
||||
|
||||
---
|
||||
|
||||
## P2 开发阶段 - 数据市场
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T017 | 流量包管理 | 导出CSV功能 | ✅ | /data-market/packages |
|
||||
| T018 | API服务 | 13个端点文档 | ✅ | /data-market/api |
|
||||
|
||||
---
|
||||
|
||||
## P2 开发阶段 - 系统监控
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T019 | 监控概览 | 真实API数据 | ✅ | /monitoring |
|
||||
| T020 | 健康检查 | 服务状态 | ✅ | /monitoring/health |
|
||||
| T021 | 告警中心 | 告警列表 | ✅ | /monitoring/alerts |
|
||||
|
||||
---
|
||||
|
||||
## P2 开发阶段 - API
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T022 | 标签API | $sample采样优化 | ✅ | /api/tags |
|
||||
| T023 | 流量池API | user_evaluation_score | ✅ | /api/traffic-packages |
|
||||
| T024 | 人群圈选API | 按项目分类 | ✅ | /api/crowd-pools |
|
||||
| T025 | 数据库结构API | 血缘节点生成 | ✅ | /api/database-structure |
|
||||
| T026 | 渠道对接API | 飞书集成 | ✅ | /api/channels |
|
||||
| T027 | 监控API | 健康/告警/指标 | ✅ | /api/monitoring |
|
||||
|
||||
---
|
||||
|
||||
## P3 执行阶段 - 项目管理
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T028 | 项目管理工作区 | .apm目录 | ✅ | .apm/* |
|
||||
| T029 | 架构图 | 系统架构文档 | ✅ | .apm/docs/architecture.md |
|
||||
| T030 | 十目录展开 | skill格式+核心提取 | ✅ | 开发文档/1-10/*.md |
|
||||
|
||||
---
|
||||
|
||||
## P4 联调阶段
|
||||
|
||||
| 任务ID | 任务模块 | 具体行动 | 状态 | 交付物 |
|
||||
|:---|:---|:---|:---:|:---|
|
||||
| T031 | 飞书集成测试 | 环境变量配置 | ⏳ | 测试报告 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 最近更新
|
||||
|
||||
- **2026-01-31 18:30** 用skill格式整理开发文档
|
||||
- **2026-01-31 18:00** 新增渠道API和监控API
|
||||
- **2026-01-31 17:30** 创建项目管理工作区
|
||||
- **2026-01-31 16:30** 完成5大模块真实数据对接
|
||||
- **2026-01-30 23:00** 完成核心功能开发
|
||||
- **2026-01-29 20:00** 项目初始化
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [项目状态](../.apm/project-state.md)
|
||||
- [执行表](../.apm/execution-table.md)
|
||||
- [对话记录](../.apm/conversation-log.md)
|
||||
- [架构图](../.apm/docs/architecture.md)
|
||||
|
||||
---
|
||||
|
||||
## 任务统计
|
||||
|
||||
```
|
||||
总计: 31 个任务
|
||||
✅ Done: 28 个 (90%)
|
||||
🔄 In Progress: 1 个
|
||||
⏳ Pending: 2 个
|
||||
❌ Blocked: 0 个
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*版本: v1.4.0 | 更新: 2026-01-31 | 维护: 卡若AI*
|
||||
276
开发文档/02_提示词文档.md
@@ -1,276 +0,0 @@
|
||||
# 神射手数据中台 - 提示词文档
|
||||
|
||||
> 📅 最后更新: 2026-01-31 18:30
|
||||
> 🎯 记录每次对话的提示词和时间节点
|
||||
> 📝 每次对话前查看历史提示词,了解上下文
|
||||
|
||||
---
|
||||
|
||||
## 一、提示词索引
|
||||
|
||||
| 时间 | 主题 | 关键词 |
|
||||
|------|------|--------|
|
||||
| 2026-01-31 18:30 | 项目检查运行+文档整理 | skill整理、项目运行、API验证 |
|
||||
| 2026-01-31 17:30 | 项目管理工作区 | .apm、GitHub调研、架构图 |
|
||||
| 2026-01-31 23:30 | 标签体系全面完善 | 用户画像、流量池分类、标签CRUD、清洗算法 |
|
||||
| 2026-01-31 19:00 | 全量完善项目 | 卡若AI项目管理、文档更新、SKILL同步 |
|
||||
| 2026-01-31 18:00 | 项目文档体系 | 文档、规则、记忆、UI优化 |
|
||||
| 2026-01-31 17:00 | 功能开发完善 | 血缘可视化、渠道配置步骤 |
|
||||
| 2026-01-31 16:00 | PRD需求开发 | AI标签引擎、人群圈选、API |
|
||||
| 2026-01-30 | 数据库优化 | 索引、缓存、查询加速 |
|
||||
| 2026-01-29 | 用户查询 | QQ查询、微博UID、手机号 |
|
||||
|
||||
---
|
||||
|
||||
## 二、详细提示词记录
|
||||
|
||||
### 2026-01-31 23:30 - 标签体系全面完善
|
||||
|
||||
**用户提示词:**
|
||||
```
|
||||
那个完善整个那个标签体系的这个包括用户画像里面都是要真实的数据,然后这里边的用户画像是可以直接创建用户画像的内容的,创建完之后它是直接分配给那个 SKR,然后来查询用户的那个生成用户的想要的有不标签的那个数据库,并且每一个用户他的流量。用户画像都可以点击进去,都可以看到详细的内容,这是一块,那第二块的话是关于流量池的这一些那个内容,流量池里面的话更多的是一个用户的一个标签,大白话一点的一个分类,并且每一个标签点击进去都是咱们 Mongo 数据库里面相对应的真实的一个标签。然后这个流量池里面的相应的人群就在咱们这里面有的所有的人群的一些真实的一个数据,那点击进去就是真实的人群的一个数据,然后更好的一个选择,然后这个圈选的流量池就由更多的一些流量池的,按这个流量池的种类进行分类。点击进去就是各个那个按标签和实际运营的热度,现在的一些那个流量池的一个内容,不要写人群圈,选这个就是流量池的一个内容,帮我把这一个整个的标签画下来,板块帮我按照这个完善一下,然后打通 skill 以及打通 Mango 的数据库来进行读取一下。按真实的情况来,内容更丰富一些,然后更真实一些,全部都要用真实的内容。那每个标签都可以选择,包括这个选择编辑删除的这一些,要有实际的一个作用更多的去完善,那包括标签管理,先进标签之后就是把这个标签管理弄到咱们的那个算法里面,并且把整个那个清洗标签的这个算法流程图也放到这里,这个标签管理的一个说明里面。把这些内容都统一的完善清楚。
|
||||
```
|
||||
|
||||
**执行结果:**
|
||||
- ✅ 用户画像API完善 - 支持真实数据查询、创建画像
|
||||
- ✅ 用户画像页面重构 - 真实数据、创建功能、详情查看
|
||||
- ✅ 流量池页面重构 - 按5大种类分类展示
|
||||
- ✅ 标签管理页面增强 - 编辑删除功能、清洗算法流程图
|
||||
- ✅ API新增PUT/DELETE方法支持CRUD
|
||||
- ✅ 打通MongoDB真实数据
|
||||
|
||||
**功能清单:**
|
||||
|
||||
| 模块 | 功能 | 状态 |
|
||||
|------|------|------|
|
||||
| 用户画像 | 画像模板列表(从MongoDB动态加载) | ✅ |
|
||||
| 用户画像 | 创建自定义画像(估值分/等级/省份/标签) | ✅ |
|
||||
| 用户画像 | 用户列表分页(真实数据) | ✅ |
|
||||
| 用户画像 | 用户详情查看 | ✅ |
|
||||
| 用户画像 | 分配给SKR引擎 | ✅ |
|
||||
| 流量池 | 价值分层(钻石/黄金/白银/青铜/潜力) | ✅ |
|
||||
| 流量池 | 行为活跃(高频/沉默/流失) | ✅ |
|
||||
| 流量池 | 地域分布(各省份) | ✅ |
|
||||
| 流量池 | 数据来源(存客宝/点了码/微博/QQ) | ✅ |
|
||||
| 流量池 | 生命周期(新用户/活跃/沉睡/流失) | ✅ |
|
||||
| 标签管理 | 标签列表(按分类展示) | ✅ |
|
||||
| 标签管理 | 创建标签(支持规则定义) | ✅ |
|
||||
| 标签管理 | 编辑标签 | ✅ |
|
||||
| 标签管理 | 删除标签(保护系统标签) | ✅ |
|
||||
| 标签管理 | 清洗规则配置(7条规则) | ✅ |
|
||||
| 标签管理 | 算法流程图(5步+RFM说明) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-31 19:00 - 全量完善项目
|
||||
|
||||
**用户提示词:**
|
||||
```
|
||||
已经建立完成了,你把这个上面的上一条,咱们的一个对话的那些需求全量把这个神射手的这个项目完善起来,全量的完善,然后用卡露 AI 的项目管理来完善整个边,完善整个开发文档,然后删除,完善完之后,嗯,分别把这些内容填写进去,一步一部分填写,能告诉我那个进度,把上面的所有的那个需求没完成的帮我把它完成掉
|
||||
```
|
||||
|
||||
**执行结果:**
|
||||
- 梳理全部对话历史需求
|
||||
- 对比SKILL和前端路由
|
||||
- 全量更新 `00_项目核心文档.md`
|
||||
- 全量更新 `01_开发进度文档.md`
|
||||
- 更新提示词文档
|
||||
- 使用卡若AI项目管理规范
|
||||
|
||||
**完成进度:**
|
||||
- ✅ 项目核心文档全量更新
|
||||
- ✅ 开发进度文档全量更新
|
||||
- ✅ 提示词文档更新
|
||||
- ✅ SKILL文档待同步
|
||||
- ✅ 系统监控模块已有
|
||||
- ✅ 用户画像模板已有
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-31 18:00 - 项目文档体系
|
||||
|
||||
**用户提示词:**
|
||||
```
|
||||
然后把这些数据库的整个的那个结构以及相应的那个结构都变成一个,一个这个项目的一个文档和一个文档,然后这个项目的文档每一次调取之后都需要是知道你要都需要调调那个读,以及读取完之后优化,也要优化到这个文档里面,然后把这个文档做的更清晰一些。我们对话中提到的几个,包括那个 AI 的这一个管家数据接入跟数据接入读取和数据分析,以及相应的那个数据查询的这一个管家的那个规则,包括这个用户资产数字化的规则都在这个项目里面。那每一次那个读取之前都要看一下数据库跟看一下这几张表,然后了解 SKU 的这个 AI SKU 的一个作用,然后再来每次读取都要看一下这些内容,然后再来更新迭代整个项目,以及项目的整个的目标运转目标都要更新迭代一下,然后把这个生成相应的几个文档。并且更新到科室的那个规则里面,这个项目的科室规则里面。整个那个都是以苹果毛玻璃的风格来设计,然后风格跟图标变得更丰富有趣一些,把整个界面变得更丰富有趣一些。后,并且设置一条咱们每一次对话的一些记忆的这一个功能,跟归那个优化这个功能都形成,都更新迭代到这个那个开发的那个文档里面,以及开发进度的文档里面,让整个项目能更通透的运转。并且修改成整个的那个开发文档,主要开发文档那个写入之前就先看一下开发文档的格式,然后按照他的要求写入进去修改这个规则进去,项目确保能有记忆、有步骤、有节奏的分布式的那个完成,并且新增相应的那个进度,项目管理写清楚。和落地规范写清楚,AI 打标签的这个功能就是输入提示词在数据库的允许范围内去做 AI 打标签的这么一个事情,它需要有一个输入提示词和编辑提示词的一个功能,把相应的功能帮我写进去,以及告诉我实时的一个进度跟实时的一个那个事情跟 AI 清洗是一样。需要配置清洗完的路,需要配置相应的规则。那智能报告也是一样,需要有一个配置规则的地方,每一个配置规则有一条默认的规则。另外一个记录的过程中,帮我把每一次那个对话的那个提示词,以及上面我们对话的所有的提示词,按时间节点都给我写一个提示词的一个文档,每一次都写入,按时间节点写入来完善这个提示词的一个文档,那么一次读取的时候也看一下这个上下的那个阶段。以及相应的那个执行时间,防止出错,那这个也都放到开发文档的进度里面
|
||||
```
|
||||
|
||||
**执行结果:**
|
||||
- 创建 `00_项目核心文档.md` - 数据库结构+AI规则
|
||||
- 创建 `01_开发进度文档.md` - 项目管理与落地规范
|
||||
- 创建 `02_提示词文档.md` - 本文档
|
||||
- 待创建 Cursor规则文件
|
||||
- 待优化 AI打标/清洗/报告的规则配置功能
|
||||
- 待优化 UI毛玻璃风格
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-31 17:00 - 功能开发完善
|
||||
|
||||
**用户提示词:**
|
||||
```
|
||||
继续
|
||||
```
|
||||
|
||||
**执行结果:**
|
||||
- 完成数据血缘可拖拉拽可视化
|
||||
- 添加渠道配置详细步骤指南
|
||||
- 8个节点 + 10条连接线
|
||||
- 飞书/企微/微信/API完整配置步骤
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-31 16:00 - PRD需求开发
|
||||
|
||||
**用户提示词:**
|
||||
```
|
||||
AI标签系统V1产品需求文档... 一个 AI 标签的系统整个的扭转是那个从数据源的管理,它是接入数据库之后就开始拆解符合的咱们的那个 AI 的那个中台的那个标签引擎...
|
||||
```
|
||||
|
||||
**执行结果:**
|
||||
- 创建 AI标签引擎页面 `/data-ingestion/ai-engine`
|
||||
- 更新首页AI对话(思考过程+用户画像模板)
|
||||
- 创建人群圈选页面(参考巨量引擎5步筛选)
|
||||
- 更新智能报告(Skill执行+报告模板)
|
||||
- 创建流量包发送(邮箱/飞书/微信)
|
||||
- 创建API服务页面(13个端点)
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-30 - 数据库优化
|
||||
|
||||
**关键提示词摘要:**
|
||||
- 优化MongoDB查询性能
|
||||
- 创建索引(qq、phone、uid)
|
||||
- 4层缓存架构设计
|
||||
- 断网自动切换本地模型
|
||||
|
||||
**执行结果:**
|
||||
- 索引优化脚本
|
||||
- 缓存机制设计
|
||||
- 容灾备份方案
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-29 - 用户查询
|
||||
|
||||
**关键提示词摘要:**
|
||||
- 查询QQ号 28533368
|
||||
- 查询手机号 13779954946
|
||||
- 微博热点UID查询
|
||||
- 2025年明星热度分析
|
||||
|
||||
**执行结果:**
|
||||
- QQ关联手机号查询
|
||||
- 用户画像完整输出
|
||||
- 微博UID批量查询
|
||||
- 明星数据分析报告
|
||||
|
||||
---
|
||||
|
||||
## 三、AI打标默认提示词
|
||||
|
||||
### 3.1 价值类打标
|
||||
```
|
||||
分析用户数据,根据RFM评分生成价值标签:
|
||||
- RFM≥90: 高价值用户
|
||||
- RFM≥70: 优质用户
|
||||
- RFM≥50: 普通用户
|
||||
- RFM<50: 待激活用户
|
||||
- 近30天无活跃: 流失风险
|
||||
```
|
||||
|
||||
### 3.2 行为类打标
|
||||
```
|
||||
分析用户行为数据,生成行为标签:
|
||||
- 日活跃: 高频用户
|
||||
- 周活跃: 活跃用户
|
||||
- 月活跃: 普通用户
|
||||
- 30天+未活跃: 沉默用户
|
||||
- 首次访问: 新用户
|
||||
```
|
||||
|
||||
### 3.3 偏好类打标
|
||||
```
|
||||
分析用户消费和行为数据,生成偏好标签:
|
||||
- 电商消费≥5次/月: 电商活跃
|
||||
- 社交互动≥10次/天: 社交达人
|
||||
- 金融产品浏览: 金融偏好
|
||||
- 游戏时长≥2h/天: 游戏玩家
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、AI清洗默认规则
|
||||
|
||||
### 4.1 手机号清洗
|
||||
```
|
||||
规则: 格式化为11位
|
||||
- 去除空格、横杠
|
||||
- 去除+86前缀
|
||||
- 验证1[3-9]开头
|
||||
- 无效手机号标记为null
|
||||
```
|
||||
|
||||
### 4.2 姓名清洗
|
||||
```
|
||||
规则: 姓名脱敏
|
||||
- 保留姓氏
|
||||
- 中间名用*替代
|
||||
- 示例: 张三 → 张*
|
||||
```
|
||||
|
||||
### 4.3 地址清洗
|
||||
```
|
||||
规则: 标准化省市
|
||||
- 提取省份名称
|
||||
- 提取城市名称
|
||||
- 去除"省""市"后缀
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、智能报告默认规则
|
||||
|
||||
### 5.1 日报规则
|
||||
```
|
||||
报告名称: 每日运营日报
|
||||
生成时间: 每日23:59
|
||||
包含章节:
|
||||
- 核心指标(总用户、活跃用户、新增用户)
|
||||
- 渠道消息(飞书、企微、微信消息量)
|
||||
- AI查询统计(查询次数、响应时间)
|
||||
- 异常告警
|
||||
```
|
||||
|
||||
### 5.2 周报规则
|
||||
```
|
||||
报告名称: 周度数据质量报告
|
||||
生成时间: 每周日00:00
|
||||
包含章节:
|
||||
- 质量概览
|
||||
- 清洗统计(清洗量、去重量)
|
||||
- 数据完整度
|
||||
- 问题数据统计
|
||||
```
|
||||
|
||||
### 5.3 月报规则
|
||||
```
|
||||
报告名称: 月度用户资产报告
|
||||
生成时间: 每月1日08:00
|
||||
包含章节:
|
||||
- 资产总览
|
||||
- 用户增长趋势
|
||||
- RFM分布变化
|
||||
- 流量池变化
|
||||
- 标签覆盖率
|
||||
- 下月预测
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、下次对话注意事项
|
||||
|
||||
1. **先读取文档**: 开始前读取 `00_项目核心文档.md` 和 `01_开发进度文档.md`
|
||||
2. **确认当前任务**: 查看进度表中的🔄状态任务
|
||||
3. **记录提示词**: 对话结束后更新本文档
|
||||
4. **更新进度**: 完成任务后标记✅并填写日期
|
||||
@@ -1,17 +1,26 @@
|
||||
# 10、项目管理
|
||||
|
||||
> 高级 PM | 执行表
|
||||
> 高级 PM | **开发文档统一入口** | 实时进度与每日进度
|
||||
|
||||
## 本目录文档
|
||||
---
|
||||
|
||||
## 📌 实时查看开发进度
|
||||
|
||||
| 文档 | 说明 |
|
||||
|:---|:---|
|
||||
| [_智能展开.md](./_智能展开.md) | 管理引擎激活 |
|
||||
| [执行表.md](./执行表.md) | 任务清单(链接.apm) |
|
||||
| **[开发进度.md](./开发进度.md)** | **唯一进度源**:任务清单 + **每日开发进度**(每次开发后必填) |
|
||||
| [优化与迭代.md](./优化与迭代.md) | 神射手优化迭代事项(已完成 / 待办) |
|
||||
| [执行表.md](./执行表.md) | 任务清单(链接 .apm) |
|
||||
| [项目核心文档.md](./项目核心文档.md) | 项目总览、技术栈、十目录说明 |
|
||||
| [提示词存档.md](./提示词存档.md) | 对话提示词与时间节点 |
|
||||
| [十目录索引.md](./十目录索引.md) | 开发文档仅保留 10 个目录的索引 |
|
||||
| [项目管理提示词.md](./项目管理提示词.md) | 管理模板 |
|
||||
| [_智能展开.md](./_智能展开.md) | 管理引擎激活 |
|
||||
|
||||
## 联动
|
||||
---
|
||||
|
||||
- 上游: 1-需求、2-架构、4-前端、6-后端
|
||||
- 主文档: `../../.apm/execution-table.md`
|
||||
- 指令: `@项目管理 查看进度`
|
||||
## 规范
|
||||
|
||||
- **每次开发后**:更新 [开发进度.md](./开发进度.md) 顶部「每日开发进度」一行,并同步任务状态。
|
||||
- 上游: 1-需求、2-架构、4-前端、6-后端;主执行表: `../../.apm/execution-table.md`。
|
||||
- 指令: `@项目管理 查看进度` → 读开发进度与执行表。
|
||||
|
||||
36
开发文档/10、项目管理/优化与迭代.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 神射手 - 优化与迭代
|
||||
|
||||
> 📅 更新:2026-02-19
|
||||
> 🎯 优化迭代事项统一列管,与 [开发进度](./开发进度.md) 联动
|
||||
|
||||
---
|
||||
|
||||
## 一、已完成的优化迭代
|
||||
|
||||
| 时间 | 事项 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| 2026-02-19 | NAS 部署 | Dockerfile、docker-compose.nas、deploy-to-nas.sh;数据库使用 NAS MongoDB |
|
||||
| 2026-02-19 | AI 智能对话统一搜索 | 手机/QQ/身份证/姓名统一解析;完整画像(地址、统一标签);多人最多前 10 条 |
|
||||
| 2026-02-19 | Charts 组件 | 补全 BarChart、PieChart 导出,修复构建 |
|
||||
| 2026-02-19 | 开发文档收敛 | 仅保留十目录;根目录文档并入 10、项目管理 与 9、手册 |
|
||||
| 2026-01-31 | 标签/流量池/用户画像 | 真实数据、CRUD、清洗算法流程图 |
|
||||
| 2026-01-31 | 渠道/监控 API | 飞书集成、健康检查、告警 |
|
||||
|
||||
---
|
||||
|
||||
## 二、待办优化与迭代
|
||||
|
||||
| 优先级 | 事项 | 状态 | 备注 |
|
||||
|:---:|:---|:---:|:---|
|
||||
| 高 | 飞书/企微环境变量配置与联调 | ⏳ | M4 交付物 |
|
||||
| 高 | NAS 首次部署验证(192.168.1.201:3117) | ⏳ | 与 NAS 同网执行 deploy-to-nas.sh |
|
||||
| 中 | 身份证字段 | 可选 | KR.用户估值 若有 id_card/身份证 则搜索已支持 |
|
||||
| 中 | 大表查询性能 | 监控 | 已用 $sample 缓解 |
|
||||
| 低 | UI 毛玻璃与图标丰富度 | ⏳ | 按产品需求排期 |
|
||||
|
||||
---
|
||||
|
||||
## 三、与开发进度联动
|
||||
|
||||
- 完成一项优化后:在 [开发进度.md](./开发进度.md) 顶部「每日开发进度」增加一行,并在本表「已完成的优化迭代」增加一行。
|
||||
- 新增优化需求:写入本表「待办优化与迭代」,并在开发进度中拆为具体任务(可选)。
|
||||
45
开发文档/10、项目管理/十目录索引.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 神射手数据中台 - 十目录统一索引
|
||||
|
||||
> 📅 更新: 2026-02-19
|
||||
> 🎯 **开发文档仅保留以下 10 个目录**,统一入口在 [本目录 README](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
开发文档/
|
||||
├── 1、需求/ # 业务需求、技术需求、成本
|
||||
├── 2、架构/ # 系统架构、技术选型、数据库
|
||||
├── 3、原型/ # 页面结构、原型规范
|
||||
├── 4、前端/ # 核心组件、前端规范
|
||||
├── 5、接口/ # API 清单与规范
|
||||
├── 6、后端/ # MongoDB 连接器、后端规范
|
||||
├── 7、数据库/ # ER、查询逻辑、管理规范
|
||||
├── 8、部署/ # 启动脚本、NAS 部署、自动化部署
|
||||
├── 9、手册/ # 快速使用、工具与模板说明
|
||||
└── 10、项目管理/ # 开发进度、优化与迭代、执行表、核心文档、提示词、本索引
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 跨目录联动
|
||||
|
||||
1、需求 → 2、架构 → 3、原型;2、架构 → 5、接口、7、数据库;5、接口 → 4、前端、6、后端;8、部署、9、手册;10、项目管理 汇总进度与优化。
|
||||
|
||||
---
|
||||
|
||||
## 📋 快速指令
|
||||
|
||||
| 目录 | 说明 |
|
||||
|:---:|:---|
|
||||
| 10 | **查看进度**:[开发进度](./开发进度.md)(含每日进度)、[优化与迭代](./优化与迭代.md)、[执行表](./执行表.md) |
|
||||
| 1~9 | 需求 / 架构 / 原型 / 前端 / 接口 / 后端 / 数据库 / 部署 / 手册 见各目录 README |
|
||||
|
||||
---
|
||||
|
||||
## 运行与端口
|
||||
|
||||
- 神射手本地:`pnpm dev`,端口 3117
|
||||
- NAS:http://192.168.1.201:3117
|
||||
- MongoDB:27017
|
||||
101
开发文档/10、项目管理/开发进度.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 神射手数据中台 - 开发进度
|
||||
|
||||
> 📅 最后更新:见下方「每日开发进度」
|
||||
> 🎯 **实时查看**:本表为唯一进度来源;每次开发后请更新「每日开发进度」与任务状态
|
||||
|
||||
---
|
||||
|
||||
## 📆 每日开发进度(每次开发后必填)
|
||||
|
||||
| 日期 | 当日完成 | 进行中 | 备注 |
|
||||
|:---|:---|:---|:---|
|
||||
| 2026-02-26 | API文档系统:新增 /api/docs 端点提供 OpenAPI 3.0 和 Markdown 格式文档;支持下载和AI直接对接;文档页面增加复制链接、下载按钮、AI对接提示卡片 | — | /api/docs?format=openapi |
|
||||
| 2026-02-26 | 开放API优化:侧边栏移除API密钥/文档子项(整合到API服务内);开放接口增强(实时数据流可视化、字段级授权管理、Webhook配置、5列统计卡片、成功率/响应时间、接入方详情增强) | — | 页面 /data-market/open-api |
|
||||
| 2026-02-26 | 卡若前端标准:神射手+毛狐狸的布局/颜色/毛玻璃/组件/特效提炼为标准文档,写入卡若AI「全栈开发」;全栈开发 Skill 与前端生成 Skill 全量更新,后续项目按此标准开发 | — | 标准见 卡若AI/全栈开发/前端标准_神射手与毛狐狸.md |
|
||||
| 2026-02-26 | 前端界面截图:开发文档/4、前端/截图 下已截首页、AI分析、数据市场、标签画像、估值模型(各视口+长图);截图索引与待截图路由清单已建 | 其余页面待补全 | 全站截图归档至 4、前端 |
|
||||
| 2026-02-26 | 数据库连接认证:MONGODB_URI 带 authSource=admin、.env.local 使用 admin/admin123;无认证 URI 时兜底默认 | — | 修复 command find requires authentication |
|
||||
| 2026-02-26 | 本地 Docker 部署:docker-compose.yml(website 分组)、神射手+MongoDB 容器;Dockerfile 与 popover 修复 | — | 本地 Docker、website 分组 |
|
||||
| 2026-02-19 | 开发文档整理:仅保留十目录;优化与迭代、开发进度迁入 10、项目管理;根目录文档合并至 10 与 9 | — | 文档结构收敛 |
|
||||
| 2026-02-19 | AI 智能对话:统一搜索(手机/QQ/身份证/姓名)、完整画像、多人前 10 条、地址与统一标签 | — | 首页与 API |
|
||||
| 2026-02-19 | NAS 部署:Dockerfile、docker-compose.nas、deploy-to-nas.sh;Charts 修复 BarChart/PieChart | — | 部署与构建 |
|
||||
| 2026-01-31 | 十目录 skill 格式、渠道 API、监控 API、项目管理工作区 | — | 文档与 API |
|
||||
| 2026-01-31 | 5 大模块真实数据对接、标签/流量池/用户画像 CRUD | — | 数据对接 |
|
||||
|
||||
*上方按日期倒序;新增日期请插入本表首行。*
|
||||
|
||||
---
|
||||
|
||||
## 📈 进度摘要
|
||||
|
||||
| 指标 | 数值 |
|
||||
|:---|:---|
|
||||
| 总任务数 | 31 |
|
||||
| 已完成 | 28 (90%) |
|
||||
| 进行中 | 1 |
|
||||
| 待开始 | 2 |
|
||||
| 阻塞中 | 0 |
|
||||
|
||||
### 进度条
|
||||
|
||||
`[██████████████████░░] 90%`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 当前里程碑
|
||||
|
||||
| 里程碑 | 目标日期 | 状态 | 交付物 |
|
||||
|:---|:---|:---:|:---|
|
||||
| M1 基础框架 | 01-29 | ✅ | Next.js项目+MongoDB连接 |
|
||||
| M2 核心功能 | 01-30 | ✅ | 5大模块前端页面 |
|
||||
| M3 真实数据 | 01-31 | ✅ | API对接真实数据库 |
|
||||
| M4 外部集成 | 02-03 | 🔄 | 飞书/企微对接 |
|
||||
| M5 正式上线 | 02-07 | 🔄 | 生产环境部署(NAS 已就绪) |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 当前风险/阻碍
|
||||
|
||||
| 问题 | 严重度 | 状态 | 处理方案 |
|
||||
|:---|:---:|:---:|:---|
|
||||
| 飞书机器人未配置 | 🟡 | 监控中 | 需配置环境变量 |
|
||||
| API响应慢(大表) | 🟢 | 已缓解 | 使用$sample采样 |
|
||||
|
||||
---
|
||||
|
||||
## 状态说明
|
||||
|
||||
| 状态 | 含义 |
|
||||
|:---:|:---|
|
||||
| ✅ | Done - 已完成 |
|
||||
| 🔄 | In Progress - 进行中 |
|
||||
| ⏳ | Pending - 待开始 |
|
||||
| ❌ | Blocked - 阻塞中 |
|
||||
|
||||
---
|
||||
|
||||
## P1~P4 任务清单(节选)
|
||||
|
||||
- **P1** T001~T003 项目初始化、数据库连接、AI对话API ✅
|
||||
- **P2 数据概览** T004 首页 ✅
|
||||
- **P2 数据接入** T005~T009 数据源、AI引擎、清洗、任务、血缘 ✅
|
||||
- **P2 标签画像** T010~T012 标签、画像、人群圈选 ✅
|
||||
- **P2 AI Agent** T013~T016 渠道、打标、清洗、报告 ✅
|
||||
- **P2 数据市场** T017~T018 流量包、API服务 ✅
|
||||
- **P2 系统监控** T019~T021 监控、健康、告警 ✅
|
||||
- **P2 API** T022~T027 标签/流量池/人群/结构/渠道/监控 API ✅
|
||||
- **P3 项目管理** T028~T030 .apm、架构图、十目录 ✅
|
||||
- **P4 联调** T031 飞书集成测试 ⏳
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [执行表](./执行表.md)(链接 .apm)
|
||||
- [优化与迭代](./优化与迭代.md)
|
||||
- [项目核心文档](./项目核心文档.md)
|
||||
- 项目状态:`../../.apm/project-state.md`
|
||||
- 架构图:`../../.apm/docs/architecture.md`
|
||||
|
||||
---
|
||||
|
||||
**规范**:每次开发完成请更新本页顶部「每日开发进度」一行,并同步任务状态与里程碑。
|
||||
71
开发文档/10、项目管理/提示词存档.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 神射手数据中台 - 提示词存档
|
||||
|
||||
> 📅 最后更新: 2026-02-26
|
||||
> 🎯 记录每次对话的提示词与时间节点;对话前可查历史
|
||||
|
||||
---
|
||||
|
||||
## 一、提示词索引
|
||||
|
||||
| 时间 | 主题 | 关键词 |
|
||||
|------|------|--------|
|
||||
| 2026-02-26 | 神射手+毛狐狸布局/功能/风格/细节→卡若前端标准,更新全栈开发与前端生成 Skill | 前端标准、神射手、毛狐狸、全站开发、颜色布局风格特效、Skill 更新 |
|
||||
| 2026-02-26 | 神射手所有界面截图到 4、前端,每页视口+长图 | 前端截图、开发文档/4、前端、长图短图 |
|
||||
| 2026-02-26 | 数据库连接认证、账号密码 admin/admin123 | 数据库连接、MONGODB_URI、authSource=admin、卡若AI |
|
||||
| 2026-02-26 | 神射手部署到本地 Docker、website 分组 | Docker、docker-compose、website、MongoDB、3117 |
|
||||
| 2026-02-19 | 开发文档收敛、十目录、每日进度 | 项目管理、优化迭代、合并删除 |
|
||||
| 2026-02-19 | AI 对话统一搜索、完整画像、前10条 | 手机、QQ、身份证、姓名、多人 |
|
||||
| 2026-02-19 | NAS 部署 ckbnas | 部署、192.168.1.201、MongoDB、Docker |
|
||||
| 2026-01-31 | 项目检查运行+文档整理 | skill整理、项目运行、API验证 |
|
||||
| 2026-01-31 | 标签体系全面完善 | 用户画像、流量池、标签CRUD |
|
||||
| 2026-01-31 | 全量完善项目 | 卡若AI项目管理、开发文档 |
|
||||
| 2026-01-29 | 用户查询 | QQ、手机号、微博UID |
|
||||
|
||||
---
|
||||
|
||||
## 二、详细提示词记录(节选)
|
||||
|
||||
### 2026-02-26 - 神射手+毛狐狸前端标准写入卡若AI
|
||||
|
||||
- 用户:把神射手的布局、功能、风格和毛狐狸的所有细节变成标准,放到卡若AI 全站开发的前端部分;颜色、布局、风格、特效作为开发标准;更新前端相关 Skill,全量更新,方便以后所有项目做参考。
|
||||
- 结果:在卡若AI `04_卡火(火)/火炬_全栈消息/全栈开发/` 下新增《前端标准_神射手与毛狐狸.md》(布局、颜色、背景、卡片、按钮、气泡、底部导航、globals.css 毛玻璃类、移动端触达、组件形态、参考项目路径);全栈开发 Skill 增加 1.4 前端标准(神射手/毛狐狸)必读与参考实现、已吸收 reference 表;前端生成 Skill 增加「卡若标准(神射手/毛狐狸)」整节(风格要点、页面容器、统计卡片、模块入口卡片、主按钮、毛玻璃 CSS)、配色表与联动说明;SKILL 触发词补充 神射手、毛狐狸、前端标准;神射手开发进度与提示词存档已更新。
|
||||
|
||||
### 2026-02-26 - 神射手全站界面截图至 4、前端
|
||||
|
||||
- 用户:把神射手所有的界面截图到这个前端里面,打开神射手所有的界面、所有的功能,每一个页面从长图到短图全部截图。
|
||||
- 结果:在开发文档/4、前端/截图 下完成 5 个页面的视口图+长图(首页、ai-analysis、data-market、tag-portrait、value-model);新增 截图/截图索引.md(已截与待补全清单)、截图/待截图路由.txt(逐条路由列表);README 已链到截图索引。部分页面因浏览器截图超时未截,可依待截图路由本地补全。
|
||||
|
||||
### 2026-02-26 - 数据库连接认证
|
||||
|
||||
- 用户:数据库连接,账号密码找卡若AI 拿,处理一下,admin admin123。结果:lib/mongodb.ts 默认与兜底均使用带 authSource=admin 的 URI;无认证的 MONGODB_URI 时自动用 admin:admin123 默认;新增 .env.local(MONGODB_URI=…authSource=admin);.env.example 与项目核心文档、卡若AI 唯一MongoDB约定 补充数据库账号与连接说明。
|
||||
|
||||
### 2026-02-26 - 本地 Docker 部署(website 分组)
|
||||
|
||||
- 把神射手部署到本地 Docker 并运行;Docker 里放 website 分组,所有网站迁移到该组,神射手也放入。结果:新增 docker-compose.yml(name: website),含 shensheshou 与 mongodb 服务;Dockerfile 改为仅 COPY package.json 以在 Linux 下正确安装 SWC;移除 package.json 中 @next/swc-darwin-arm64;补全 app/components/ui/popover.tsx;部署文档增加「五、本地 Docker 运行」;容器 website-shensheshou、website-mongodb 已启动,访问 http://localhost:3117 返回 200。
|
||||
|
||||
### 2026-02-19 - 开发文档只保留十目录
|
||||
|
||||
- 优化迭代放入 10、项目管理;开发文档只保留 10 个目录;同类型合并到 10 个目录下;重复与不需要内容删除;实时看到开发进度,包括每日开发进度,每次都需要开发进度。
|
||||
|
||||
### 2026-02-19 - AI 智能对话完善
|
||||
|
||||
- 关键字搜索任意数据;完整用户画像;AI 分析;手机/QQ/身份证/姓名统一搜索;可多人,列出前 10 条;地址、统一标签、曼谷库数据完善。
|
||||
|
||||
### 2026-02-19 - NAS 部署
|
||||
|
||||
- 部署到 ckbnas 192.168.1.201,数据库使用 NAS。结果:Dockerfile、docker-compose.nas、deploy-to-nas.sh、Charts 修复等。
|
||||
|
||||
---
|
||||
|
||||
## 三、AI 打标 / 清洗 / 报告默认规则
|
||||
|
||||
(保留原 02_提示词文档 中的 3.1~3.3、4.1~4.3、5.1~5.3 小节内容,此处略;可自原备份恢复或从版本历史查看。)
|
||||
|
||||
---
|
||||
|
||||
## 四、下次对话注意事项
|
||||
|
||||
1. **先读文档**: [项目核心文档](./项目核心文档.md)、[开发进度](./开发进度.md)(含每日进度)
|
||||
2. **确认任务**: 查看开发进度中 🔄 状态任务与 [优化与迭代](./优化与迭代.md)
|
||||
3. **记录提示词**: 对话结束后更新本文档
|
||||
4. **更新进度**: 完成任务后更新 [开发进度](./开发进度.md) 顶部「每日开发进度」与任务状态
|
||||
72
开发文档/10、项目管理/项目核心文档.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 神射手数据中台 - 项目核心文档
|
||||
|
||||
> 📅 最后更新:2026-02-26
|
||||
> 🎯 项目总览、技术栈、API、数据库;详细见各目录
|
||||
|
||||
---
|
||||
|
||||
## 📊 项目概览
|
||||
|
||||
| 项目 | 信息 |
|
||||
|:---|:---|
|
||||
| 项目名称 | 神射手数据中台 |
|
||||
| 项目描述 | 用户资产数字化中台,整合20亿+用户数据的查询、标签、画像和流量池管理 |
|
||||
| 当前阶段 | P3 执行 |
|
||||
| 前端路径 | 神射手项目根目录 |
|
||||
| 开发文档 | 仅保留 **1~10 共 10 个目录**,入口见 [README](./README.md) |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术栈
|
||||
|
||||
- **前端**: Next.js 14、Radix UI、TailwindCSS、Recharts;风格:苹果毛玻璃
|
||||
- **后端**: Next.js API Routes(25+ 端点)、MongoDB
|
||||
- **数据**: 26 个 KR_* 库、20 亿+ 条、222GB
|
||||
|
||||
---
|
||||
|
||||
## 📁 开发文档十目录
|
||||
|
||||
| 目录 | 内容 |
|
||||
|:---|:---|
|
||||
| 1、需求 | 业务/技术需求、成本 |
|
||||
| 2、架构 | 系统架构、技术选型、数据库 |
|
||||
| 3、原型 | 页面结构、原型规范 |
|
||||
| 4、前端 | 核心组件、前端规范 |
|
||||
| 5、接口 | API 清单与规范 |
|
||||
| 6、后端 | MongoDB 连接器、后端规范 |
|
||||
| 7、数据库 | ER、查询逻辑、管理规范 |
|
||||
| 8、部署 | 启动脚本、NAS 部署、自动化部署 |
|
||||
| 9、手册 | 快速使用、工具与模板说明 |
|
||||
| 10、项目管理 | **开发进度、优化与迭代、执行表、本核心文档、提示词存档、十目录索引** |
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 数据库连接
|
||||
|
||||
- **账号密码**:见卡若AI《00_账号与API索引》二、本机 MongoDB(统一):`admin` / `admin123`
|
||||
- **连接串**:`MONGODB_URI=mongodb://admin:admin123@localhost:27017/?authSource=admin`(本地);必须带 `authSource=admin`,否则会报 `command find requires authentication`
|
||||
- **配置位置**:项目根目录 `.env.local`(勿提交 Git)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速启动
|
||||
|
||||
```bash
|
||||
# 本地
|
||||
cd 神射手 && pnpm dev # 端口 3117
|
||||
|
||||
# NAS(与 NAS 同网)
|
||||
./scripts/deploy-to-nas.sh # 访问 http://192.168.1.201:3117
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 本目录文档
|
||||
|
||||
- [开发进度](./开发进度.md) - **实时进度与每日进度**
|
||||
- [优化与迭代](./优化与迭代.md)
|
||||
- [执行表](./执行表.md)
|
||||
- [提示词存档](./提示词存档.md)
|
||||
- [十目录索引](./十目录索引.md)
|
||||
- [项目管理提示词](./项目管理提示词.md)
|
||||
@@ -9,6 +9,7 @@
|
||||
| [_智能展开.md](./_智能展开.md) | 前端引擎激活、iOS风格 |
|
||||
| [核心组件代码.md](./核心组件代码.md) | **从项目提取**:AI对话、毛玻璃样式 |
|
||||
| [前端开发规范.md](./前端开发规范.md) | 开发规范 |
|
||||
| [截图/截图索引.md](./截图/截图索引.md) | **界面截图索引**:各页面视口图与长图清单、待补全路由 |
|
||||
|
||||
## 联动
|
||||
|
||||
|
||||
BIN
开发文档/4、前端/截图/01-首页-视口.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
开发文档/4、前端/截图/01-首页-长图.png
Normal file
|
After Width: | Height: | Size: 363 KiB |
BIN
开发文档/4、前端/截图/02-AI分析-视口.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
开发文档/4、前端/截图/02-AI分析-长图.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
开发文档/4、前端/截图/03-数据市场-视口.png
Normal file
|
After Width: | Height: | Size: 414 KiB |
BIN
开发文档/4、前端/截图/03-数据市场-长图.png
Normal file
|
After Width: | Height: | Size: 441 KiB |
BIN
开发文档/4、前端/截图/04-标签画像-视口.png
Normal file
|
After Width: | Height: | Size: 414 KiB |
BIN
开发文档/4、前端/截图/04-标签画像-长图.png
Normal file
|
After Width: | Height: | Size: 441 KiB |
BIN
开发文档/4、前端/截图/05-估值模型-视口.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
开发文档/4、前端/截图/05-估值模型-长图.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
开发文档/4、前端/截图/06-设置-视口.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
开发文档/4、前端/截图/06-设置-长图.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
开发文档/4、前端/截图/07-文档-视口.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
开发文档/4、前端/截图/07-文档-长图.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
开发文档/4、前端/截图/08-登录-视口.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
开发文档/4、前端/截图/08-登录-长图.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
开发文档/4、前端/截图/09-流量池-视口.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
开发文档/4、前端/截图/09-流量池-长图.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
开发文档/4、前端/截图/10-用户画像-视口.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
开发文档/4、前端/截图/10-用户画像-长图.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
开发文档/4、前端/截图/11-用户画像-标签-视口.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
开发文档/4、前端/截图/11-用户画像-标签-长图.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
开发文档/4、前端/截图/12-用户画像详情-视口.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
开发文档/4、前端/截图/12-用户画像详情-长图.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
开发文档/4、前端/截图/13-标签画像-标签-视口.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
开发文档/4、前端/截图/13-标签画像-标签-长图.png
Normal file
|
After Width: | Height: | Size: 430 KiB |
BIN
开发文档/4、前端/截图/14-标签画像-画像-视口.png
Normal file
|
After Width: | Height: | Size: 431 KiB |
BIN
开发文档/4、前端/截图/14-标签画像-画像-长图.png
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
开发文档/4、前端/截图/15-标签画像-人群-视口.png
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
开发文档/4、前端/截图/15-标签画像-人群-长图.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
开发文档/4、前端/截图/16-数据资产-视口.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
开发文档/4、前端/截图/16-数据资产-长图.png
Normal file
|
After Width: | Height: | Size: 425 KiB |
BIN
开发文档/4、前端/截图/17-数据资产-包-视口.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
开发文档/4、前端/截图/17-数据资产-包-长图.png
Normal file
|
After Width: | Height: | Size: 519 KiB |
BIN
开发文档/4、前端/截图/18-数据资产-API市场-视口.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
开发文档/4、前端/截图/18-数据资产-API市场-长图.png
Normal file
|
After Width: | Height: | Size: 645 KiB |
BIN
开发文档/4、前端/截图/19-数据市场-包-视口.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
开发文档/4、前端/截图/19-数据市场-包-长图.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
开发文档/4、前端/截图/20-数据市场-API-视口.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
开发文档/4、前端/截图/20-数据市场-API-长图.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
开发文档/4、前端/截图/21-数据市场-开放API-视口.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
开发文档/4、前端/截图/21-数据市场-开放API-长图.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
开发文档/4、前端/截图/22-模型列表-视口.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
开发文档/4、前端/截图/22-模型列表-长图.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
开发文档/4、前端/截图/23-价值评估-视口.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
开发文档/4、前端/截图/23-价值评估-长图.png
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
开发文档/4、前端/截图/24-估值报表-视口.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
开发文档/4、前端/截图/24-估值报表-长图.png
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
开发文档/4、前端/截图/25-价值评估页-视口.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
开发文档/4、前端/截图/25-价值评估页-长图.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
开发文档/4、前端/截图/26-用户估值-视口.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
开发文档/4、前端/截图/26-用户估值-长图.png
Normal file
|
After Width: | Height: | Size: 355 KiB |
BIN
开发文档/4、前端/截图/27-用户价值-视口.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
开发文档/4、前端/截图/27-用户价值-长图.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
开发文档/4、前端/截图/28-数据集成-视口.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
开发文档/4、前端/截图/28-数据集成-长图.png
Normal file
|
After Width: | Height: | Size: 429 KiB |
BIN
开发文档/4、前端/截图/29-数据接入-视口.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
开发文档/4、前端/截图/29-数据接入-长图.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
开发文档/4、前端/截图/30-数据接入-数据源-视口.png
Normal file
|
After Width: | Height: | Size: 419 KiB |
BIN
开发文档/4、前端/截图/30-数据接入-数据源-长图.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |