From b96acadf9184b1d655819a98222553e675eb8b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Thu, 5 Feb 2026 11:35:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=B4=E9=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/distribution/page.tsx | 29 +- app/admin/page.tsx | 44 +- app/api/miniprogram/pay/notify/route.ts | 87 +- app/api/miniprogram/pay/route.ts | 68 +- app/api/orders/route.ts | 2 + lib/db.ts | 1 + miniprogram/app.js | 16 + miniprogram/pages/match/match.js | 5 +- miniprogram/pages/read/read.js | 10 +- miniprogram/pages/read/read.wxml | 4 +- scripts/check_chunks.py | 46 + scripts/check_nginx.py | 24 + scripts/check_server_logs.py | 41 + scripts/check_static_files.py | 102 + scripts/devlop.py | 43 +- scripts/devlopTest.py | 729 ++++ scripts/fix_port_issue.py | 60 + scripts/restart_pm2.py | 29 + scripts/restart_soul_correctly.py | 79 + scripts/start-standalone.js | 54 +- 开发文档/8、部署/分销与绑定流程图.md | 383 ++ 开发文档/8、部署/支付订单修复总结.md | 399 -- 开发文档/8、部署/支付订单未创建问题分析.md | 488 --- 开发文档/8、部署/管理端静态资源404排查.md | 263 ++ 开发文档/8、部署/邀请码分销规则说明.md | 78 +- 开发文档/8、部署/部署脚本备份/README.md | 7 - 开发文档/8、部署/部署脚本备份/devlop.py | 192 - 开发文档/9、手册/提示词/落地方案提示词.md | 5 - 开发文档/9、手册/提示词/说明手册提示词.md | 5 - 开发文档/产研团队 第21场 20260129 许永平.txt | 3738 ------------------ 开发文档/当前小程序开发细则.md | 165 + 31 files changed, 2263 insertions(+), 4933 deletions(-) create mode 100644 scripts/check_chunks.py create mode 100644 scripts/check_nginx.py create mode 100644 scripts/check_server_logs.py create mode 100644 scripts/check_static_files.py create mode 100644 scripts/devlopTest.py create mode 100644 scripts/fix_port_issue.py create mode 100644 scripts/restart_pm2.py create mode 100644 scripts/restart_soul_correctly.py create mode 100644 开发文档/8、部署/分销与绑定流程图.md delete mode 100644 开发文档/8、部署/支付订单修复总结.md delete mode 100644 开发文档/8、部署/支付订单未创建问题分析.md create mode 100644 开发文档/8、部署/管理端静态资源404排查.md delete mode 100644 开发文档/8、部署/部署脚本备份/README.md delete mode 100644 开发文档/8、部署/部署脚本备份/devlop.py delete mode 100644 开发文档/9、手册/提示词/落地方案提示词.md delete mode 100644 开发文档/9、手册/提示词/说明手册提示词.md delete mode 100644 开发文档/产研团队 第21场 20260129 许永平.txt create mode 100644 开发文档/当前小程序开发细则.md diff --git a/app/admin/distribution/page.tsx b/app/admin/distribution/page.tsx index 38da7239..1f34cc48 100644 --- a/app/admin/distribution/page.tsx +++ b/app/admin/distribution/page.tsx @@ -88,6 +88,9 @@ interface Order { status: 'pending' | 'completed' | 'failed' paymentMethod?: string referrerEarnings?: number + referrerId?: string | null + /** 下单时记录的邀请码(订单表 referral_code) */ + referralCode?: string | null createdAt: string } @@ -131,13 +134,18 @@ export default function DistributionAdminPage() { const ordersRes = await fetch('/api/orders') const ordersData = await ordersRes.json() if (ordersData.success && ordersData.orders) { - // 补充用户信息 + // 补充用户信息与推荐人信息 const enrichedOrders = ordersData.orders.map((order: Order) => { const user = usersArr.find((u: User) => u.id === order.userId) + const referrer = order.referrerId + ? usersArr.find((u: User) => u.id === order.referrerId) + : null return { ...order, userNickname: user?.nickname || '未知用户', - userPhone: user?.phone || '-' + userPhone: user?.phone || '-', + referrerNickname: referrer?.nickname || null, + referrerCode: referrer?.referral_code || null, } }) setOrders(enrichedOrders) @@ -565,6 +573,7 @@ export default function DistributionAdminPage() { 金额 支付方式 状态 + 推荐人/邀请码 分销佣金 下单时间 @@ -579,7 +588,9 @@ export default function DistributionAdminPage() { order.id?.toLowerCase().includes(term) || order.userNickname?.toLowerCase().includes(term) || order.userPhone?.includes(term) || - order.sectionTitle?.toLowerCase().includes(term) + order.sectionTitle?.toLowerCase().includes(term) || + (order.referrerCode && order.referrerCode.toLowerCase().includes(term)) || + (order.referrerNickname && order.referrerNickname.toLowerCase().includes(term)) ) } return true @@ -617,14 +628,22 @@ export default function DistributionAdminPage() { order.paymentMethod || '微信支付'} - {order.status === 'completed' ? ( + {order.status === 'completed' || order.status === 'paid' ? ( 已完成 - ) : order.status === 'pending' ? ( + ) : order.status === 'pending' || order.status === 'created' ? ( 待支付 ) : ( 已失败 )} + + {order.referrerId || order.referralCode ? ( + + {order.referrerNickname || order.referralCode || order.referrerCode || order.referrerId?.slice(0, 8)} + {(order.referralCode || order.referrerCode) ? ` (${order.referralCode || order.referrerCode})` : ''} + + ) : '-'} + {order.referrerEarnings ? `¥${order.referrerEarnings.toFixed(2)}` : '-'} diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 172adb51..a3c7af2b 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -67,6 +67,15 @@ export default function AdminDashboard() { const totalUsers = users.length const totalPurchases = purchases.length + // 订单类型对应中文(product_type: section | fullbook | match) + const productTypeLabel = (p: { productType?: string; productId?: string; sectionTitle?: string }) => { + const type = p.productType || "" + if (type === "section") return p.productId ? `单章 ${p.productId}` : "单章" + if (type === "fullbook") return "整本购买" + if (type === "match") return "找伙伴" + return p.sectionTitle || "其他" + } + const stats = [ { title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20", link: "/admin/users" }, { @@ -125,21 +134,28 @@ export default function AdminDashboard() { {purchases .slice(-5) .reverse() - .map((p) => ( -
-
-

{p.sectionTitle || "整本购买"}

-

{new Date(p.createdAt).toLocaleString()}

+ .map((p) => { + const referrer = p.referrerId && users.find((u: any) => u.id === p.referrerId) + const inviteCode = p.referralCode || referrer?.referral_code || referrer?.nickname || p.referrerId?.slice(0, 8) + return ( +
+
+

{productTypeLabel(p)}

+

{new Date(p.createdAt).toLocaleString()}

+ {inviteCode && ( +

邀请码: {inviteCode}

+ )} +
+
+

+¥{p.amount}

+

{p.paymentMethod || "微信支付"}

+
-
-

+¥{p.amount}

-

{p.paymentMethod || "微信支付"}

-
-
- ))} + ) + })} {purchases.length === 0 &&

暂无订单数据

}
diff --git a/app/api/miniprogram/pay/notify/route.ts b/app/api/miniprogram/pay/notify/route.ts index 6d9bd4f4..408a0940 100644 --- a/app/api/miniprogram/pay/notify/route.ts +++ b/app/api/miniprogram/pay/notify/route.ts @@ -122,7 +122,31 @@ export async function POST(request: Request) { } } - const { productType, productId, userId } = attach + const { productType, productId, userId: attachUserId } = attach + + // 买家身份必须以微信 openId 为准(不可伪造),避免客户端伪造 userId 导致错误归属/分佣 + let buyerUserId: string | undefined = attachUserId + if (openId) { + try { + const usersByOpenId = await query('SELECT id FROM users WHERE open_id = ?', [openId]) as any[] + if (usersByOpenId.length > 0) { + const resolvedId = usersByOpenId[0].id + if (attachUserId && resolvedId !== attachUserId) { + console.warn('[PayNotify] 买家身份校验: attach.userId 与 openId 解析不一致,以 openId 为准', { + attachUserId, + resolvedId, + orderSn, + }) + } + buyerUserId = resolvedId + } + } catch (e) { + console.error('[PayNotify] 按 openId 解析买家失败:', e) + } + } + if (!buyerUserId && attachUserId) { + buyerUserId = attachUserId + } // 1. 更新订单状态为已支付 let orderExists = false @@ -143,33 +167,55 @@ export async function POST(request: Request) { INSERT INTO orders ( id, order_sn, user_id, open_id, product_type, product_id, amount, description, - status, transaction_id, pay_time, referrer_id, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + status, transaction_id, pay_time, referrer_id, referral_code, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, NULL, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `, [ - orderSn, orderSn, userId || openId, openId, + orderSn, orderSn, buyerUserId || openId, openId, productType || 'unknown', productId || '', totalAmount, '支付回调补记订单', transactionId ]) console.log('[PayNotify] ✅ 订单补记成功:', orderSn) orderExists = true } catch (insertErr: any) { - if (insertErr?.message?.includes('referrer_id') || insertErr?.code === 'ER_BAD_FIELD_ERROR') { + const msg = insertErr?.message || '' + const code = insertErr?.code || '' + if (msg.includes('referrer_id') || msg.includes('referral_code') || code === 'ER_BAD_FIELD_ERROR') { try { await query(` INSERT INTO orders ( id, order_sn, user_id, open_id, product_type, product_id, amount, description, - status, transaction_id, pay_time, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + status, transaction_id, pay_time, referrer_id, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `, [ - orderSn, orderSn, userId || openId, openId, + orderSn, orderSn, buyerUserId || openId, openId, productType || 'unknown', productId || '', totalAmount, '支付回调补记订单', transactionId ]) - console.log('[PayNotify] ✅ 订单补记成功(无 referrer_id):', orderSn) + console.log('[PayNotify] ✅ 订单补记成功(无 referral_code):', orderSn) orderExists = true - } catch (e2) { - console.error('[PayNotify] ❌ 补记订单失败:', e2) + } catch (e2: any) { + if (e2?.message?.includes('referrer_id') || e2?.code === 'ER_BAD_FIELD_ERROR') { + try { + await query(` + INSERT INTO orders ( + id, order_sn, user_id, open_id, + product_type, product_id, amount, description, + status, transaction_id, pay_time, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, [ + orderSn, orderSn, buyerUserId || openId, openId, + productType || 'unknown', productId || '', totalAmount, + '支付回调补记订单', transactionId + ]) + console.log('[PayNotify] ✅ 订单补记成功(无 referrer_id/referral_code):', orderSn) + orderExists = true + } catch (e3) { + console.error('[PayNotify] ❌ 补记订单失败:', e3) + } + } else { + console.error('[PayNotify] ❌ 补记订单失败:', e2) + } } } else { console.error('[PayNotify] ❌ 补记订单失败:', insertErr) @@ -199,20 +245,7 @@ export async function POST(request: Request) { console.error('[PayNotify] ❌ 处理订单失败:', e) } - // 2. 获取用户信息 - let buyerUserId = userId - if (!buyerUserId && openId) { - try { - const users = await query('SELECT id FROM users WHERE open_id = ?', [openId]) as any[] - if (users.length > 0) { - buyerUserId = users[0].id - } - } catch (e) { - console.error('[PayNotify] 获取用户信息失败:', e) - } - } - - // 3. 更新用户购买记录(✅ 检查是否已有其他相同产品的已支付订单) + // 2. 更新用户购买记录(buyerUserId 已在上面以 openId 为准解析)(✅ 检查是否已有其他相同产品的已支付订单) if (buyerUserId && productType) { try { if (productType === 'fullbook') { @@ -256,7 +289,7 @@ export async function POST(request: Request) { console.error('[PayNotify] ❌ 更新用户购买记录失败:', e) } - // 4. 清理相同产品的无效订单(未支付的订单) + // 3. 清理相同产品的无效订单(未支付的订单) if (productType && (productType === 'fullbook' || productId)) { try { const deleteResult = await query(` @@ -288,7 +321,7 @@ export async function POST(request: Request) { } } - // 5. 处理分销佣金(90%给推广者) + // 4. 处理分销佣金(90%给推广者) await processReferralCommission(buyerUserId, totalAmount, orderSn) } diff --git a/app/api/miniprogram/pay/route.ts b/app/api/miniprogram/pay/route.ts index c056a329..30707051 100644 --- a/app/api/miniprogram/pay/route.ts +++ b/app/api/miniprogram/pay/route.ts @@ -166,6 +166,17 @@ export async function POST(request: Request) { } catch (e) { console.warn('[MiniPay] 查询推荐人失败,继续创建订单:', e) } + + // 下单时使用的邀请码:优先用请求体,否则用推荐人当前邀请码(便于订单记录对账) + let orderReferralCode: string | null = body.referralCode ? String(body.referralCode).trim() || null : null + if (!orderReferralCode && referrerId) { + try { + const refRows = (await query(`SELECT referral_code FROM users WHERE id = ? LIMIT 1`, [referrerId]) as any[]) + if (refRows.length > 0 && refRows[0].referral_code) { + orderReferralCode = refRows[0].referral_code + } + } catch (_) { /* 忽略 */ } + } try { // 检查是否已有相同产品的已支付订单 @@ -186,34 +197,55 @@ export async function POST(request: Request) { }) } - // 插入订单(含 referrer_id,便于分销归属与统计) + // 插入订单(含 referrer_id、referral_code,便于分销归属与对账) try { await query(` INSERT INTO orders ( id, order_sn, user_id, open_id, product_type, product_id, amount, description, - status, transaction_id, referrer_id, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + status, transaction_id, referrer_id, referral_code, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) `, [ orderSn, orderSn, userId, openId, productType, productId || 'fullbook', amount, goodsBody, - 'created', null, referrerId + 'created', null, referrerId, orderReferralCode ]) } catch (insertErr: any) { - // 兼容:若表尚无 referrer_id 列,则用不含该字段的 INSERT - if (insertErr?.message?.includes('referrer_id') || insertErr?.code === 'ER_BAD_FIELD_ERROR') { - await query(` - INSERT INTO orders ( - id, order_sn, user_id, open_id, - product_type, product_id, amount, description, - status, transaction_id, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) - `, [ - orderSn, orderSn, userId, openId, - productType, productId || 'fullbook', amount, goodsBody, - 'created', null - ]) - console.log('[MiniPay] 订单已插入(未含 referrer_id,请执行 scripts/add_orders_referrer_id.py)') + // 兼容:若表尚无 referrer_id 或 referral_code 列 + const msg = (insertErr as any)?.message || '' + const code = (insertErr as any)?.code || '' + if (msg.includes('referrer_id') || msg.includes('referral_code') || code === 'ER_BAD_FIELD_ERROR') { + try { + await query(` + INSERT INTO orders ( + id, order_sn, user_id, open_id, + product_type, product_id, amount, description, + status, transaction_id, referrer_id, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + `, [ + orderSn, orderSn, userId, openId, + productType, productId || 'fullbook', amount, goodsBody, + 'created', null, referrerId + ]) + console.log('[MiniPay] 订单已插入(未含 referral_code,请执行 scripts/add_orders_referral_code.py)') + } catch (e2: any) { + if (e2?.message?.includes('referrer_id') || e2?.code === 'ER_BAD_FIELD_ERROR') { + await query(` + INSERT INTO orders ( + id, order_sn, user_id, open_id, + product_type, product_id, amount, description, + status, transaction_id, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + `, [ + orderSn, orderSn, userId, openId, + productType, productId || 'fullbook', amount, goodsBody, + 'created', null + ]) + console.log('[MiniPay] 订单已插入(未含 referrer_id/referral_code,请执行迁移脚本)') + } else { + throw e2 + } + } } else { throw insertErr } diff --git a/app/api/orders/route.ts b/app/api/orders/route.ts index 99fa16d4..3f7d8a2c 100644 --- a/app/api/orders/route.ts +++ b/app/api/orders/route.ts @@ -22,6 +22,8 @@ function rowToOrder(row: Record) { status: row.status, transactionId: row.transaction_id, payTime: row.pay_time, + referrerId: row.referrer_id ?? null, + referralCode: row.referral_code ?? null, createdAt: row.created_at, updatedAt: row.updated_at, } diff --git a/lib/db.ts b/lib/db.ts index 1dc11bba..fb50a273 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -156,6 +156,7 @@ export async function initDatabase() { transaction_id VARCHAR(100), pay_time TIMESTAMP NULL, referrer_id VARCHAR(50) NULL COMMENT '推荐人用户ID,用于分销归属', + referral_code VARCHAR(20) NULL COMMENT '下单时使用的邀请码,便于对账与展示', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), diff --git a/miniprogram/app.js b/miniprogram/app.js index d96e266e..2fe0ad5a 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -89,6 +89,8 @@ App({ // 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺) this.globalData.pendingReferralCode = refCode wx.setStorageSync('pendingReferralCode', refCode) + // 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code + wx.setStorageSync('referral_code', refCode) // 如果已登录,立即尝试绑定,由 /api/referral/bind 按 30 天规则决定 new / renew / takeover if (this.globalData.isLoggedIn && this.globalData.userInfo) { @@ -354,6 +356,20 @@ App({ if (res.success && res.data?.openId) { this.globalData.openId = res.data.openId wx.setStorageSync('openId', res.data.openId) + // 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码 + if (res.data.user) { + this.globalData.userInfo = res.data.user + this.globalData.isLoggedIn = true + this.globalData.purchasedSections = res.data.user.purchasedSections || [] + this.globalData.hasFullBook = res.data.user.hasFullBook || false + wx.setStorageSync('userInfo', res.data.user) + wx.setStorageSync('token', res.data.token || '') + const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode + if (pendingRef) { + console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef) + this.bindReferralCode(pendingRef) + } + } return res.data.openId } } catch (e) { diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js index 245158d5..7e0007d3 100644 --- a/miniprogram/pages/match/match.js +++ b/miniprogram/pages/match/match.js @@ -591,6 +591,8 @@ Page({ return } + // 邀请码:与章节支付一致,写入订单便于分销归属与对账 + const referralCode = wx.getStorageSync('referral_code') || '' // 调用支付接口购买匹配次数 const res = await app.request('/api/miniprogram/pay', { method: 'POST', @@ -600,7 +602,8 @@ Page({ productId: 'match_1', amount: 1, description: '匹配次数x1', - userId: app.globalData.userInfo?.id || '' + userId: app.globalData.userInfo?.id || '', + referralCode: referralCode || undefined } }) diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index eae16ac0..6b59a99e 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -91,8 +91,8 @@ Page({ const config = await accessManager.fetchLatestConfig() this.setData({ freeIds: config.freeChapters, - sectionPrice: config.prices.section, - fullBookPrice: config.prices.fullbook + sectionPrice: config.prices?.section ?? 1, + fullBookPrice: config.prices?.fullbook ?? 9.9 }) // 【标准流程】2. 确定权限状态 @@ -162,6 +162,10 @@ Page({ async loadContent(id, accessState) { try { const section = this.getSectionInfo(id) + const sectionPrice = this.data.sectionPrice ?? 1 + if (section.price === undefined || section.price === null) { + section.price = sectionPrice + } this.setData({ section }) // 从 API 获取内容 @@ -689,7 +693,7 @@ Page({ ? '《一场Soul的创业实验》全书' : `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}` - // 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),用于订单分销归属 + // 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),会写入订单 referrer_id / referral_code 便于分销与对账 const referralCode = wx.getStorageSync('referral_code') || '' const res = await app.request('/api/miniprogram/pay', { method: 'POST', diff --git a/miniprogram/pages/read/read.wxml b/miniprogram/pages/read/read.wxml index 9c70d7c1..eb4183a8 100644 --- a/miniprogram/pages/read/read.wxml +++ b/miniprogram/pages/read/read.wxml @@ -166,7 +166,7 @@ 购买本章 - ¥{{section.price}} + ¥{{section && section.price != null ? section.price : sectionPrice}} @@ -176,7 +176,7 @@ 解锁全部 {{totalSections}} 章 - ¥{{fullBookPrice}} + ¥{{fullBookPrice || 9.9}} 省82% diff --git a/scripts/check_chunks.py b/scripts/check_chunks.py new file mode 100644 index 00000000..c5d4b9c0 --- /dev/null +++ b/scripts/check_chunks.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import paramiko + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15) + +print("=== 检查 chunks 目录文件(前20个)===") +cmd = "ls -la /www/wwwroot/soul/.next/static/chunks/ 2>/dev/null | head -25" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "目录不存在") + +print("\n=== 是否有 turbopack 文件 ===") +cmd = "find /www/wwwroot/soul/.next/static -name '*turbopack*' 2>/dev/null" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "无 turbopack 文件(正常,这是生产模式)") + +print("\n=== 检查请求的具体文件 ===") +files_to_check = [ + "a954454d2ab1d3ca.css", + "6a98f5c6b2554ef3.js", + "turbopack-0d89ab930ad9d74d.js", +] +for f in files_to_check: + cmd = "find /www/wwwroot/soul/.next/static -name '%s' 2>/dev/null" % f + stdin, stdout, stderr = client.exec_command(cmd, timeout=10) + result = stdout.read().decode('utf-8', errors='replace').strip() + status = "[OK] 存在" if result else "[X] 不存在" + print("%s: %s" % (f, status)) + +print("\n=== 检查实际可用的 css 文件 ===") +cmd = "ls /www/wwwroot/soul/.next/static/css/ 2>/dev/null | head -10" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "无 css 文件") + +print("\n=== 构建模式检查 ===") +cmd = "head -5 /www/wwwroot/soul/.next/BUILD_ID 2>/dev/null" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print("BUILD_ID: %s" % (result if result else "不存在")) + +client.close() diff --git a/scripts/check_nginx.py b/scripts/check_nginx.py new file mode 100644 index 00000000..c21028b1 --- /dev/null +++ b/scripts/check_nginx.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import paramiko + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15) + +print("=== soul.quwanzhi.com.conf ===") +stdin, stdout, stderr = client.exec_command('cat /www/server/panel/vhost/nginx/soul.quwanzhi.com.conf 2>/dev/null', timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "文件不存在") + +print("\n=== 检查 include 配置 ===") +stdin, stdout, stderr = client.exec_command('ls -la /www/server/panel/vhost/nginx/ | grep soul', timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "无 soul 相关配置") + +print("\n=== node_soul.conf ===") +stdin, stdout, stderr = client.exec_command('cat /www/server/panel/vhost/nginx/node_soul.conf 2>/dev/null', timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "文件不存在") + +client.close() diff --git a/scripts/check_server_logs.py b/scripts/check_server_logs.py new file mode 100644 index 00000000..21905b3d --- /dev/null +++ b/scripts/check_server_logs.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import paramiko + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15) + +print("=== PM2 soul 日志(最后 50 行)===") +cmd = "pm2 logs soul --lines 50 --nostream 2>&1 | tail -50" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +print("\n=== PM2 soul 错误日志 ===") +cmd = "pm2 logs soul --err --lines 30 --nostream 2>&1 | tail -30" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +print("\n=== 检查 server.js 文件 ===") +cmd = "ls -lh /www/wwwroot/soul/server.js" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +print("\n=== 检查 .next 目录结构 ===") +cmd = "ls -lh /www/wwwroot/soul/.next/ | head -20" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +print("\n=== 检查端口 30006 ===") +cmd = "curl -I http://127.0.0.1:30006 2>&1 | head -10" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +client.close() diff --git a/scripts/check_static_files.py b/scripts/check_static_files.py new file mode 100644 index 00000000..2137a753 --- /dev/null +++ b/scripts/check_static_files.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +快速检查服务器上静态资源是否存在 +用于排查管理端 404 问题 +""" + +import os +import sys + +try: + import paramiko +except ImportError: + print("请安装: pip install paramiko") + sys.exit(1) + +# 配置(与 devlop.py 一致) +DEPLOY_PROJECT_PATH = os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul") +DEVLOP_DIST_PATH = "/www/wwwroot/auto-devlop/soul/dist" +DEFAULT_SSH_PORT = int(os.environ.get("DEPLOY_SSH_PORT", "22022")) + +def get_cfg(): + return { + "host": os.environ.get("DEPLOY_HOST", "42.194.232.22"), + "user": os.environ.get("DEPLOY_USER", "root"), + "password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"), + "ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""), + "project_path": os.environ.get("DEPLOY_PROJECT_PATH", DEPLOY_PROJECT_PATH), + } + +def check_static_files(): + cfg = get_cfg() + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + print("正在连接服务器...") + if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]): + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15) + else: + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15) + + print("\n=== 检查静态资源目录 ===") + + # 检查多个可能的路径(deploy 模式和 devlop 模式) + checks = [ + ("%s/.next/static" % DEVLOP_DIST_PATH, "devlop 模式 dist 目录"), + ("%s/.next/static" % DEPLOY_PROJECT_PATH, "deploy 模式项目目录"), + ("%s/server.js" % DEVLOP_DIST_PATH, "devlop server.js"), + ("%s/server.js" % DEPLOY_PROJECT_PATH, "deploy server.js"), + ] + + for path, desc in checks: + # 检查文件或目录是否存在 + cmd = "test -e '%s' && echo 'EXISTS' || echo 'NOT_FOUND'" % path + stdin, stdout, stderr = client.exec_command(cmd, timeout=10) + result = stdout.read().decode("utf-8", errors="replace").strip() + status = "[OK]" if "EXISTS" in result else "[X]" + print("%s %s" % (status, desc)) + print(" 路径: %s" % path) + + if "EXISTS" in result and "static" in path: + # 列出文件数量 + cmd2 = "find '%s' -type f 2>/dev/null | wc -l" % path + stdin2, stdout2, stderr2 = client.exec_command(cmd2, timeout=10) + file_count = stdout2.read().decode("utf-8", errors="replace").strip() + print(" 文件数: %s" % file_count) + + print("\n=== 检查 PM2 项目配置 ===") + cmd = "pm2 describe soul 2>/dev/null | grep -E 'cwd|script|status' | head -5 || echo 'PM2 soul 不存在'" + stdin, stdout, stderr = client.exec_command(cmd, timeout=10) + pm2_info = stdout.read().decode("utf-8", errors="replace").strip() + print(pm2_info) + + print("\n=== 检查端口监听 ===") + cmd = "ss -tlnp | grep 30006 || echo '端口 30006 未监听'" + stdin, stdout, stderr = client.exec_command(cmd, timeout=10) + port_info = stdout.read().decode("utf-8", errors="replace").strip() + print(port_info) + + print("\n=== 检查 Nginx 反向代理 ===") + cmd = "grep -r 'proxy_pass' /www/server/panel/vhost/nginx/*soul* 2>/dev/null | head -3 || echo '未找到 soul Nginx 配置'" + stdin, stdout, stderr = client.exec_command(cmd, timeout=10) + nginx_info = stdout.read().decode("utf-8", errors="replace").strip() + print(nginx_info) + + print("\n" + "=" * 50) + print("诊断建议:") + print("1. devlop 模式部署后,PM2 的 cwd 应为: %s" % DEVLOP_DIST_PATH) + print("2. .next/static 必须在 PM2 的 cwd 目录下") + print("3. Nginx 必须整站反代(location /),不能只反代 /api") + print("4. 浏览器强刷: Ctrl+Shift+R 清除缓存") + + except Exception as e: + print("错误: %s" % str(e)) + import traceback + traceback.print_exc() + finally: + client.close() + +if __name__ == "__main__": + check_static_files() diff --git a/scripts/devlop.py b/scripts/devlop.py index 99c16f0f..bfc7999f 100644 --- a/scripts/devlop.py +++ b/scripts/devlop.py @@ -36,7 +36,7 @@ except ImportError: # 端口统一从环境变量 DEPLOY_PORT 读取,未设置时使用此默认值 DEPLOY_PM2_APP = "soul" -DEFAULT_DEPLOY_PORT = 30006 +DEFAULT_DEPLOY_PORT = 3888 DEPLOY_PROJECT_PATH = "/www/wwwroot/soul" DEPLOY_SITE_URL = "https://soul.quwanzhi.com" # SSH 端口(支持环境变量 DEPLOY_SSH_PORT,未设置时默认为 22022) @@ -61,7 +61,9 @@ def get_cfg(): def get_cfg_devlop(): - """devlop 模式配置:在基础配置上增加 base_path / dist / dist2""" + """devlop 模式配置:在基础配置上增加 base_path / dist / dist2。 + 实际运行目录为 dist_path(切换后新版本在 dist),宝塔 PM2 项目路径必须指向 dist_path, + 否则会从错误目录启动导致 .next/static 等静态资源 404。""" cfg = get_cfg().copy() cfg["base_path"] = os.environ.get("DEVOP_BASE_PATH", "/www/wwwroot/auto-devlop/soul") cfg["dist_path"] = cfg["base_path"] + "/dist" @@ -269,6 +271,10 @@ def pack_standalone_tar(root): if not os.path.isdir(standalone) or not os.path.isdir(static_src): print(" [失败] 未找到 .next/standalone 或 .next/static") return None + chunks_dir = os.path.join(static_src, "chunks") + if not os.path.isdir(chunks_dir): + print(" [失败] .next/static/chunks 不存在,请先完整执行 pnpm build(本地 pnpm start 能正常打开页面后再部署)") + return None staging = tempfile.mkdtemp(prefix="soul_deploy_") try: @@ -291,6 +297,13 @@ def pack_standalone_tar(root): shutil.rmtree(static_dst) os.makedirs(os.path.dirname(static_dst), exist_ok=True) shutil.copytree(static_src, static_dst) + # 同步构建索引,避免 server 用错 BUILD_ID/manifest 导致静态 404 + next_root = os.path.join(root, ".next") + next_staging = os.path.join(staging, ".next") + for name in ["BUILD_ID", "build-manifest.json", "app-path-routes-manifest.json", "routes-manifest.json"]: + src = os.path.join(next_root, name) + if os.path.isfile(src): + shutil.copy2(src, os.path.join(next_staging, name)) if os.path.isdir(public_src): shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True) if os.path.isfile(ecosystem_src): @@ -410,7 +423,7 @@ def deploy_via_baota_api(cfg): # ==================== 打包(devlop 模式:zip) ==================== -ZIP_EXCLUDE_DIRS = {".cache", "__pycache__", ".git", "node_modules", "cache", "test", "tests", "coverage", ".nyc_output", ".turbo", "开发文档", "miniprogramPre", "my-app", "newpp"} +ZIP_EXCLUDE_DIRS = {".cache", "__pycache__", ".git", "node_modules", "cache", "test", "tests", "coverage", ".nyc_output", ".turbo", "开发文档"} ZIP_EXCLUDE_FILE_NAMES = {".DS_Store", "Thumbs.db"} ZIP_EXCLUDE_FILE_SUFFIXES = (".log", ".map") @@ -438,6 +451,10 @@ def pack_standalone_zip(root): if not os.path.isdir(standalone) or not os.path.isdir(static_src): print(" [失败] 未找到 .next/standalone 或 .next/static") return None + chunks_dir = os.path.join(static_src, "chunks") + if not os.path.isdir(chunks_dir): + print(" [失败] .next/static/chunks 不存在,请先完整执行 pnpm build(本地 pnpm start 能正常打开页面后再部署)") + return None staging = tempfile.mkdtemp(prefix="soul_devlop_") try: @@ -457,6 +474,13 @@ def pack_standalone_zip(root): break os.makedirs(os.path.join(staging, ".next"), exist_ok=True) shutil.copytree(static_src, os.path.join(staging, ".next", "static"), dirs_exist_ok=True) + # 同步构建索引,避免 server 用错 BUILD_ID/manifest 导致静态 404 + next_root = os.path.join(root, ".next") + next_staging = os.path.join(staging, ".next") + for name in ["BUILD_ID", "build-manifest.json", "app-path-routes-manifest.json", "routes-manifest.json"]: + src = os.path.join(next_root, name) + if os.path.isfile(src): + shutil.copy2(src, os.path.join(next_staging, name)) if os.path.isdir(public_src): shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True) if os.path.isfile(ecosystem_src): @@ -599,7 +623,7 @@ def run_pnpm_install_in_dist2(cfg): def remote_swap_dist_and_restart(cfg): - """暂停 → dist→dist1, dist2→dist → 删除 dist1 → 重启(devlop 模式)""" + """暂停 → dist→dist1, dist2→dist → 删除 dist1 → 更新 PM2 项目路径 → 重启(devlop 模式)""" print("[5/7] 宝塔 API 暂停 Node 项目 ...") stop_node_project(cfg["panel_url"], cfg["api_key"], cfg["pm2_name"]) time.sleep(2) @@ -620,9 +644,16 @@ def remote_swap_dist_and_restart(cfg): print(" [成功] 新版本位于 %s" % cfg["dist_path"]) finally: client.close() - print("[7/7] 宝塔 API 重启 Node 项目 ...") + # 关键:devlop 实际运行目录是 dist_path,必须让宝塔 PM2 从该目录启动,否则会从错误目录跑导致静态资源 404 + print("[7/7] 更新宝塔 Node 项目路径并重启 ...") + add_or_update_node_project( + cfg["panel_url"], cfg["api_key"], cfg["pm2_name"], + cfg["dist_path"], # 使用 dist_path,不是 project_path + port=cfg["port"], + node_path=cfg.get("node_path"), + ) if not start_node_project(cfg["panel_url"], cfg["api_key"], cfg["pm2_name"]): - print(" [警告] 请到宝塔手动启动 %s" % cfg["pm2_name"]) + print(" [警告] 请到宝塔手动启动 %s,并确认项目路径为: %s" % (cfg["pm2_name"], cfg["dist_path"])) return False return True diff --git a/scripts/devlopTest.py b/scripts/devlopTest.py new file mode 100644 index 00000000..c63af7db --- /dev/null +++ b/scripts/devlopTest.py @@ -0,0 +1,729 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import os +import sys +import shutil +import tempfile +import argparse +import json +import zipfile +import tarfile +import subprocess +import time +import hashlib + +try: + import paramiko +except ImportError: + print("错误: 请先安装 paramiko") + print(" pip install paramiko") + sys.exit(1) + +try: + import requests + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +except ImportError: + print("错误: 请先安装 requests") + print(" pip install requests") + sys.exit(1) + + +# ==================== 配置 ==================== + +# 端口统一从环境变量 DEPLOY_PORT 读取,未设置时使用此默认值 +DEPLOY_PM2_APP = "testsoul" +DEFAULT_DEPLOY_PORT = 30006 +DEPLOY_PROJECT_PATH = "/www/wwwroot/soul" +DEPLOY_SITE_URL = "https://soul.quwanzhi.com" +# SSH 端口(支持环境变量 DEPLOY_SSH_PORT,未设置时默认为 22022) +DEFAULT_SSH_PORT = int(os.environ.get("DEPLOY_SSH_PORT", "22022")) + +def get_cfg(): + """获取基础部署配置(deploy 模式与 devlop 共用 SSH/宝塔)""" + return { + "host": os.environ.get("DEPLOY_HOST", "42.194.232.22"), + "user": os.environ.get("DEPLOY_USER", "root"), + "password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"), + "ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""), + "project_path": os.environ.get("DEPLOY_PROJECT_PATH", DEPLOY_PROJECT_PATH), + "panel_url": os.environ.get("BAOTA_PANEL_URL", "https://42.194.232.22:9988"), + "api_key": os.environ.get("BAOTA_API_KEY", "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"), + "pm2_name": os.environ.get("DEPLOY_PM2_APP", DEPLOY_PM2_APP), + "site_url": os.environ.get("DEPLOY_SITE_URL", DEPLOY_SITE_URL), + "port": int(os.environ.get("DEPLOY_PORT", str(DEFAULT_DEPLOY_PORT))), + "node_version": os.environ.get("DEPLOY_NODE_VERSION", "v22.14.0"), + "node_path": os.environ.get("DEPLOY_NODE_PATH", "/www/server/nodejs/v22.14.0/bin"), + } + + +def get_cfg_devlop(): + """devlop 模式配置:在基础配置上增加 base_path / dist / dist2。 + 实际运行目录为 dist_path(切换后新版本在 dist),宝塔 PM2 项目路径必须指向 dist_path, + 否则会从错误目录启动导致 .next/static 等静态资源 404。""" + cfg = get_cfg().copy() + cfg["base_path"] = os.environ.get("DEVOP_BASE_PATH", "/www/wwwroot/auto-devlop/soulTest") + cfg["dist_path"] = cfg["base_path"] + "/dist" + cfg["dist2_path"] = cfg["base_path"] + "/dist2" + return cfg + + +# ==================== 宝塔 API ==================== + +def _get_sign(api_key): + now_time = int(time.time()) + sign_str = str(now_time) + hashlib.md5(api_key.encode("utf-8")).hexdigest() + request_token = hashlib.md5(sign_str.encode("utf-8")).hexdigest() + return now_time, request_token + + +def _baota_request(panel_url, api_key, path, data=None): + req_time, req_token = _get_sign(api_key) + payload = {"request_time": req_time, "request_token": req_token} + if data: + payload.update(data) + url = panel_url.rstrip("/") + "/" + path.lstrip("/") + try: + r = requests.post(url, data=payload, verify=False, timeout=30) + return r.json() if r.text else {} + except Exception as e: + print(" API 请求失败: %s" % str(e)) + return None + + +def get_node_project_list(panel_url, api_key): + for path in ["/project/nodejs/get_project_list", "/plugin?action=a&name=nodejs&s=get_project_list"]: + result = _baota_request(panel_url, api_key, path) + if result and (result.get("status") is True or "data" in result): + return result.get("data", []) + return None + + +def get_node_project_status(panel_url, api_key, pm2_name): + projects = get_node_project_list(panel_url, api_key) + if projects: + for p in projects: + if p.get("name") == pm2_name: + return p + return None + + +def start_node_project(panel_url, api_key, pm2_name): + for path in ["/project/nodejs/start_project", "/plugin?action=a&name=nodejs&s=start_project"]: + result = _baota_request(panel_url, api_key, path, {"project_name": pm2_name}) + if result and (result.get("status") is True or result.get("msg") or "成功" in str(result)): + print(" [成功] 启动成功: %s" % pm2_name) + return True + return False + + +def stop_node_project(panel_url, api_key, pm2_name): + for path in ["/project/nodejs/stop_project", "/plugin?action=a&name=nodejs&s=stop_project"]: + result = _baota_request(panel_url, api_key, path, {"project_name": pm2_name}) + if result and (result.get("status") is True or result.get("msg") or "成功" in str(result)): + print(" [成功] 停止成功: %s" % pm2_name) + return True + return False + + +def restart_node_project(panel_url, api_key, pm2_name): + project_status = get_node_project_status(panel_url, api_key, pm2_name) + if project_status: + print(" 项目状态: %s" % project_status.get("status", "未知")) + for path in ["/project/nodejs/restart_project", "/plugin?action=a&name=nodejs&s=restart_project"]: + result = _baota_request(panel_url, api_key, path, {"project_name": pm2_name}) + if result and (result.get("status") is True or result.get("msg") or "成功" in str(result)): + print(" [成功] 重启成功: %s" % pm2_name) + return True + if result and "msg" in result: + print(" API 返回: %s" % result.get("msg")) + print(" [警告] 重启失败,请检查宝塔 Node 插件是否安装、API 密钥是否正确") + return False + + +def add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port=None, node_path=None): + if port is None: + port = int(os.environ.get("DEPLOY_PORT", str(DEFAULT_DEPLOY_PORT))) + port_env = "PORT=%d " % port + run_cmd = port_env + ("%s/node server.js" % node_path if node_path else "node server.js") + payload = {"name": pm2_name, "path": project_path, "run_cmd": run_cmd, "port": str(port)} + for path in ["/project/nodejs/add_project", "/plugin?action=a&name=nodejs&s=add_project"]: + result = _baota_request(panel_url, api_key, path, payload) + if result and result.get("status") is True: + print(" [成功] 项目配置已更新: %s" % pm2_name) + return True + if result and "msg" in result: + print(" API 返回: %s" % result.get("msg")) + return False + + +# ==================== 本地构建 ==================== + +def run_build(root): + """执行本地 pnpm build""" + use_shell = sys.platform == "win32" + standalone = os.path.join(root, ".next", "standalone") + server_js = os.path.join(standalone, "server.js") + + try: + r = subprocess.run( + ["pnpm", "build"], + cwd=root, + shell=use_shell, + timeout=600, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + stdout_text = r.stdout or "" + stderr_text = r.stderr or "" + combined = stdout_text + stderr_text + is_windows_symlink_error = ( + sys.platform == "win32" + and r.returncode != 0 + and ("EPERM" in combined or "symlink" in combined.lower() or "operation not permitted" in combined.lower() or "errno: -4048" in combined) + ) + + if r.returncode != 0: + if is_windows_symlink_error: + print(" [警告] Windows 符号链接权限错误(EPERM)") + print(" 解决方案:开启开发者模式 / 以管理员运行 / 或使用 --no-build") + if os.path.isdir(standalone) and os.path.isfile(server_js): + print(" [成功] standalone 输出可用,继续部署") + return True + return False + print(" [失败] 构建失败,退出码:", r.returncode) + for line in (stdout_text.strip().split("\n") or [])[-10:]: + print(" " + line) + return False + except subprocess.TimeoutExpired: + print(" [失败] 构建超时(超过10分钟)") + return False + except FileNotFoundError: + print(" [失败] 未找到 pnpm,请安装: npm install -g pnpm") + return False + except Exception as e: + print(" [失败] 构建异常:", str(e)) + if os.path.isdir(standalone) and os.path.isfile(server_js): + print(" [提示] 可尝试使用 --no-build 跳过构建") + return False + + if not os.path.isdir(standalone) or not os.path.isfile(server_js): + print(" [失败] 未找到 .next/standalone 或 server.js") + return False + print(" [成功] 构建完成") + return True + + +def clean_standalone_before_build(root, retries=3, delay=2): + """构建前删除 .next/standalone,避免 Windows EBUSY""" + standalone = os.path.join(root, ".next", "standalone") + if not os.path.isdir(standalone): + return True + for attempt in range(1, retries + 1): + try: + shutil.rmtree(standalone) + print(" [清理] 已删除 .next/standalone(第 %d 次尝试)" % attempt) + return True + except (OSError, PermissionError): + if attempt < retries: + print(" [清理] 被占用,%ds 后重试 (%d/%d) ..." % (delay, attempt, retries)) + time.sleep(delay) + else: + print(" [失败] 无法删除 .next/standalone,可改用 --no-build") + return False + return False + + +# ==================== 打包(deploy 模式:tar.gz) ==================== + +def _copy_with_dereference(src, dst): + if os.path.islink(src): + link_target = os.readlink(src) + real_path = link_target if os.path.isabs(link_target) else os.path.join(os.path.dirname(src), link_target) + if os.path.exists(real_path): + if os.path.isdir(real_path): + shutil.copytree(real_path, dst, symlinks=False, dirs_exist_ok=True) + else: + shutil.copy2(real_path, dst) + else: + shutil.copy2(src, dst, follow_symlinks=False) + elif os.path.isdir(src): + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst, symlinks=False, dirs_exist_ok=True) + else: + shutil.copy2(src, dst) + + +def pack_standalone_tar(root): + """打包 standalone 为 tar.gz(deploy 模式用)""" + print("[2/4] 打包 standalone ...") + standalone = os.path.join(root, ".next", "standalone") + static_src = os.path.join(root, ".next", "static") + public_src = os.path.join(root, "public") + ecosystem_src = os.path.join(root, "ecosystem.config.cjs") + + if not os.path.isdir(standalone) or not os.path.isdir(static_src): + print(" [失败] 未找到 .next/standalone 或 .next/static") + return None + + staging = tempfile.mkdtemp(prefix="soul_deploy_") + try: + for name in os.listdir(standalone): + _copy_with_dereference(os.path.join(standalone, name), os.path.join(staging, name)) + node_modules_dst = os.path.join(staging, "node_modules") + pnpm_dir = os.path.join(node_modules_dst, ".pnpm") + if os.path.isdir(pnpm_dir): + for dep in ["styled-jsx"]: + dep_in_root = os.path.join(node_modules_dst, dep) + if not os.path.exists(dep_in_root): + for pnpm_pkg in os.listdir(pnpm_dir): + if pnpm_pkg.startswith(dep + "@"): + src_dep = os.path.join(pnpm_dir, pnpm_pkg, "node_modules", dep) + if os.path.isdir(src_dep): + shutil.copytree(src_dep, dep_in_root, symlinks=False, dirs_exist_ok=True) + break + static_dst = os.path.join(staging, ".next", "static") + if os.path.exists(static_dst): + shutil.rmtree(static_dst) + os.makedirs(os.path.dirname(static_dst), exist_ok=True) + shutil.copytree(static_src, static_dst) + if os.path.isdir(public_src): + shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True) + if os.path.isfile(ecosystem_src): + shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs")) + pkg_json = os.path.join(staging, "package.json") + if os.path.isfile(pkg_json): + try: + with open(pkg_json, "r", encoding="utf-8") as f: + data = json.load(f) + data.setdefault("scripts", {})["start"] = "node server.js" + with open(pkg_json, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + except Exception: + pass + tarball = os.path.join(tempfile.gettempdir(), "soul_deploy.tar.gz") + with tarfile.open(tarball, "w:gz") as tf: + for name in os.listdir(staging): + tf.add(os.path.join(staging, name), arcname=name) + print(" [成功] 打包完成: %s (%.2f MB)" % (tarball, os.path.getsize(tarball) / 1024 / 1024)) + return tarball + except Exception as e: + print(" [失败] 打包异常:", str(e)) + return None + finally: + shutil.rmtree(staging, ignore_errors=True) + + +# ==================== Node 环境检查 & SSH 上传(deploy 模式) ==================== + +def check_node_environments(cfg): + print("[检查] Node 环境 ...") + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if cfg.get("ssh_key"): + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15) + else: + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15) + stdin, stdout, stderr = client.exec_command("which node && node -v", timeout=10) + print(" 默认 Node: %s" % (stdout.read().decode("utf-8", errors="replace").strip() or "未找到")) + node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin") + stdin, stdout, stderr = client.exec_command("%s/node -v 2>/dev/null" % node_path, timeout=5) + print(" 配置 Node: %s" % (stdout.read().decode("utf-8", errors="replace").strip() or "不可用")) + return True + except Exception as e: + print(" [警告] %s" % str(e)) + return False + finally: + client.close() + + +def upload_and_extract(cfg, tarball_path): + """SSH 上传 tar.gz 并解压到 project_path(deploy 模式)""" + print("[3/4] SSH 上传并解压 ...") + if not cfg.get("password") and not cfg.get("ssh_key"): + print(" [失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY") + return False + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]): + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15) + else: + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15) + sftp = client.open_sftp() + remote_tar = "/tmp/soul_deploy.tar.gz" + remote_script = "/tmp/soul_deploy_extract.sh" + sftp.put(tarball_path, remote_tar) + node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin") + project_path = cfg["project_path"] + script_content = """#!/bin/bash +export PATH=%s:$PATH +cd %s +rm -rf .next public ecosystem.config.cjs server.js package.json 2>/dev/null +tar -xzf %s +rm -f %s +echo OK +""" % (node_path, project_path, remote_tar, remote_tar) + with sftp.open(remote_script, "w") as f: + f.write(script_content) + sftp.close() + client.exec_command("chmod +x %s" % remote_script, timeout=10) + stdin, stdout, stderr = client.exec_command("bash %s" % remote_script, timeout=120) + out = stdout.read().decode("utf-8", errors="replace").strip() + exit_status = stdout.channel.recv_exit_status() + if exit_status != 0 or "OK" not in out: + print(" [失败] 解压失败,退出码:", exit_status) + return False + print(" [成功] 解压完成: %s" % project_path) + return True + except Exception as e: + print(" [失败] SSH 错误:", str(e)) + return False + finally: + client.close() + + +def deploy_via_baota_api(cfg): + """宝塔 API 重启 Node 项目(deploy 模式)""" + print("[4/4] 宝塔 API 管理 Node 项目 ...") + panel_url, api_key, pm2_name = cfg["panel_url"], cfg["api_key"], cfg["pm2_name"] + project_path = cfg["project_path"] + node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin") + port = cfg["port"] + + if not get_node_project_status(panel_url, api_key, pm2_name): + add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port, node_path) + stop_node_project(panel_url, api_key, pm2_name) + time.sleep(2) + ok = restart_node_project(panel_url, api_key, pm2_name) + if not ok: + ok = start_node_project(panel_url, api_key, pm2_name) + if not ok: + print(" 请到宝塔 Node 项目手动重启 %s,路径: %s" % (pm2_name, project_path)) + return ok + + +# ==================== 打包(devlop 模式:zip) ==================== + +ZIP_EXCLUDE_DIRS = {".cache", "__pycache__", ".git", "node_modules", "cache", "test", "tests", "coverage", ".nyc_output", ".turbo", "开发文档"} +ZIP_EXCLUDE_FILE_NAMES = {".DS_Store", "Thumbs.db"} +ZIP_EXCLUDE_FILE_SUFFIXES = (".log", ".map") + + +def _should_exclude_from_zip(arcname, is_file=True): + parts = arcname.replace("\\", "/").split("/") + for part in parts: + if part in ZIP_EXCLUDE_DIRS: + return True + if is_file and parts: + name = parts[-1] + if name in ZIP_EXCLUDE_FILE_NAMES or any(name.endswith(s) for s in ZIP_EXCLUDE_FILE_SUFFIXES): + return True + return False + + +def pack_standalone_zip(root): + """打包 standalone 为 zip(devlop 模式用)""" + print("[2/7] 打包 standalone 为 zip ...") + standalone = os.path.join(root, ".next", "standalone") + static_src = os.path.join(root, ".next", "static") + public_src = os.path.join(root, "public") + ecosystem_src = os.path.join(root, "ecosystem.config.cjs") + + if not os.path.isdir(standalone) or not os.path.isdir(static_src): + print(" [失败] 未找到 .next/standalone 或 .next/static") + return None + + staging = tempfile.mkdtemp(prefix="soul_devlop_") + try: + for name in os.listdir(standalone): + _copy_with_dereference(os.path.join(standalone, name), os.path.join(staging, name)) + node_modules_dst = os.path.join(staging, "node_modules") + pnpm_dir = os.path.join(node_modules_dst, ".pnpm") + if os.path.isdir(pnpm_dir): + for dep in ["styled-jsx"]: + dep_in_root = os.path.join(node_modules_dst, dep) + if not os.path.exists(dep_in_root): + for pnpm_pkg in os.listdir(pnpm_dir): + if pnpm_pkg.startswith(dep + "@"): + src_dep = os.path.join(pnpm_dir, pnpm_pkg, "node_modules", dep) + if os.path.isdir(src_dep): + shutil.copytree(src_dep, dep_in_root, symlinks=False, dirs_exist_ok=True) + break + os.makedirs(os.path.join(staging, ".next"), exist_ok=True) + shutil.copytree(static_src, os.path.join(staging, ".next", "static"), dirs_exist_ok=True) + if os.path.isdir(public_src): + shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True) + if os.path.isfile(ecosystem_src): + shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs")) + pkg_json = os.path.join(staging, "package.json") + if os.path.isfile(pkg_json): + try: + with open(pkg_json, "r", encoding="utf-8") as f: + data = json.load(f) + data.setdefault("scripts", {})["start"] = "node server.js" + with open(pkg_json, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + except Exception: + pass + server_js = os.path.join(staging, "server.js") + if os.path.isfile(server_js): + try: + deploy_port = int(os.environ.get("DEPLOY_PORT", str(DEFAULT_DEPLOY_PORT))) + with open(server_js, "r", encoding="utf-8") as f: + c = f.read() + if "|| 3000" in c: + with open(server_js, "w", encoding="utf-8") as f: + f.write(c.replace("|| 3000", "|| %d" % deploy_port)) + except Exception: + pass + zip_path = os.path.join(tempfile.gettempdir(), "soul_devlop.zip") + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for name in os.listdir(staging): + path = os.path.join(staging, name) + if os.path.isfile(path): + if not _should_exclude_from_zip(name): + zf.write(path, name) + else: + for dirpath, dirs, filenames in os.walk(path): + dirs[:] = [d for d in dirs if not _should_exclude_from_zip(os.path.join(name, os.path.relpath(os.path.join(dirpath, d), path)), is_file=False)] + for f in filenames: + full = os.path.join(dirpath, f) + arcname = os.path.join(name, os.path.relpath(full, path)) + if not _should_exclude_from_zip(arcname): + zf.write(full, arcname) + print(" [成功] 打包完成: %s (%.2f MB)" % (zip_path, os.path.getsize(zip_path) / 1024 / 1024)) + return zip_path + except Exception as e: + print(" [失败] 打包异常:", str(e)) + return None + finally: + shutil.rmtree(staging, ignore_errors=True) + + +def upload_zip_and_extract_to_dist2(cfg, zip_path): + """上传 zip 并解压到 dist2(devlop 模式)""" + print("[3/7] SSH 上传 zip 并解压到 dist2 ...") + sys.stdout.flush() + if not cfg.get("password") and not cfg.get("ssh_key"): + print(" [失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY") + return False + zip_size_mb = os.path.getsize(zip_path) / (1024 * 1024) + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + print(" 正在连接 %s@%s:%s ..." % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT)) + sys.stdout.flush() + if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]): + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=30, banner_timeout=30) + else: + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=30, banner_timeout=30) + print(" [OK] SSH 已连接,正在上传 zip(%.1f MB)..." % zip_size_mb) + sys.stdout.flush() + remote_zip = cfg["base_path"].rstrip("/") + "/soul_devlop.zip" + sftp = client.open_sftp() + # 上传进度:每 5MB 打印一次 + chunk_mb = 5.0 + last_reported = [0] + + def _progress(transferred, total): + if total and total > 0: + now_mb = transferred / (1024 * 1024) + if now_mb - last_reported[0] >= chunk_mb or transferred >= total: + last_reported[0] = now_mb + print("\r 上传进度: %.1f / %.1f MB" % (now_mb, total / (1024 * 1024)), end="") + sys.stdout.flush() + + sftp.put(zip_path, remote_zip, callback=_progress) + if zip_size_mb >= chunk_mb: + print("") + print(" [OK] zip 已上传,正在服务器解压(约 1–3 分钟)...") + sys.stdout.flush() + sftp.close() + dist2 = cfg["dist2_path"] + cmd = "rm -rf %s && mkdir -p %s && unzip -o -q %s -d %s && rm -f %s && echo OK" % (dist2, dist2, remote_zip, dist2, remote_zip) + stdin, stdout, stderr = client.exec_command(cmd, timeout=300) + out = stdout.read().decode("utf-8", errors="replace").strip() + err = stderr.read().decode("utf-8", errors="replace").strip() + if err: + print(" 服务器 stderr: %s" % err[:500]) + exit_status = stdout.channel.recv_exit_status() + if exit_status != 0 or "OK" not in out: + print(" [失败] 解压失败,退出码: %s" % exit_status) + if out: + print(" stdout: %s" % out[:300]) + return False + print(" [成功] 已解压到: %s" % dist2) + return True + except Exception as e: + print(" [失败] SSH 错误: %s" % str(e)) + import traceback + traceback.print_exc() + return False + finally: + client.close() + + +def run_pnpm_install_in_dist2(cfg): + """服务器 dist2 内执行 pnpm install,阻塞等待完成后再返回(改目录前必须完成)""" + print("[4/7] 服务器 dist2 内执行 pnpm install(等待完成后再切换目录)...") + sys.stdout.flush() + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]): + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15) + else: + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15) + stdin, stdout, stderr = client.exec_command("bash -lc 'which pnpm'", timeout=10) + pnpm_path = stdout.read().decode("utf-8", errors="replace").strip() + if not pnpm_path: + return False, "未找到 pnpm,请服务器安装: npm install -g pnpm" + cmd = "bash -lc 'cd %s && %s install'" % (cfg["dist2_path"], pnpm_path) + stdin, stdout, stderr = client.exec_command(cmd, timeout=300) + out = stdout.read().decode("utf-8", errors="replace").strip() + err = stderr.read().decode("utf-8", errors="replace").strip() + if stdout.channel.recv_exit_status() != 0: + return False, "pnpm install 失败\n" + (err or out) + print(" [成功] dist2 内 pnpm install 已执行完成,可安全切换目录") + return True, None + except Exception as e: + return False, str(e) + finally: + client.close() + + +def remote_swap_dist_and_restart(cfg): + """暂停 → dist→dist1, dist2→dist → 删除 dist1 → 更新 PM2 项目路径 → 重启(devlop 模式)""" + print("[5/7] 宝塔 API 暂停 Node 项目 ...") + stop_node_project(cfg["panel_url"], cfg["api_key"], cfg["pm2_name"]) + time.sleep(2) + print("[6/7] 服务器切换目录: dist→dist1, dist2→dist ...") + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]): + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15) + else: + client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15) + cmd = "cd %s && mv dist dist1 2>/dev/null; mv dist2 dist && rm -rf dist1 && echo OK" % cfg["base_path"] + stdin, stdout, stderr = client.exec_command(cmd, timeout=60) + out = stdout.read().decode("utf-8", errors="replace").strip() + if stdout.channel.recv_exit_status() != 0 or "OK" not in out: + print(" [失败] 切换失败") + return False + print(" [成功] 新版本位于 %s" % cfg["dist_path"]) + finally: + client.close() + # 关键:devlop 实际运行目录是 dist_path,必须让宝塔 PM2 从该目录启动,否则会从错误目录跑导致静态资源 404 + print("[7/7] 更新宝塔 Node 项目路径并重启 ...") + add_or_update_node_project( + cfg["panel_url"], cfg["api_key"], cfg["pm2_name"], + cfg["dist_path"], # 使用 dist_path,不是 project_path + port=cfg["port"], + node_path=cfg.get("node_path"), + ) + if not start_node_project(cfg["panel_url"], cfg["api_key"], cfg["pm2_name"]): + print(" [警告] 请到宝塔手动启动 %s,并确认项目路径为: %s" % (cfg["pm2_name"], cfg["dist_path"])) + return False + return True + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description="Soul 创业派对 - 统一部署脚本", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__) + parser.add_argument("--mode", choices=["devlop", "deploy"], default="devlop", help="devlop=dist切换(默认), deploy=直接覆盖") + parser.add_argument("--no-build", action="store_true", help="跳过本地构建") + parser.add_argument("--no-upload", action="store_true", help="仅 deploy 模式:跳过 SSH 上传") + parser.add_argument("--no-api", action="store_true", help="仅 deploy 模式:上传后不调宝塔 API") + args = parser.parse_args() + + script_dir = os.path.dirname(os.path.abspath(__file__)) + root = os.path.dirname(script_dir) + + if args.mode == "devlop": + cfg = get_cfg_devlop() + print("=" * 60) + print(" Soul 自动部署(dist 切换)") + print("=" * 60) + print(" 服务器: %s@%s 目录: %s Node: %s" % (cfg["user"], cfg["host"], cfg["base_path"], cfg["pm2_name"])) + print("=" * 60) + if not args.no_build: + print("[1/7] 本地构建 pnpm build ...") + if sys.platform == "win32" and not clean_standalone_before_build(root): + return 1 + if not run_build(root): + return 1 + elif not os.path.isfile(os.path.join(root, ".next", "standalone", "server.js")): + print("[错误] 未找到 .next/standalone/server.js") + return 1 + else: + print("[1/7] 跳过本地构建") + zip_path = pack_standalone_zip(root) + if not zip_path: + return 1 + if not upload_zip_and_extract_to_dist2(cfg, zip_path): + return 1 + try: + os.remove(zip_path) + except Exception: + pass + # 必须在 dist2 内 pnpm install 执行完成后再切换目录 + ok, err = run_pnpm_install_in_dist2(cfg) + if not ok: + print(" [失败] %s" % (err or "pnpm install 失败")) + return 1 + # install 已完成,再执行 dist→dist1、dist2→dist 切换 + if not remote_swap_dist_and_restart(cfg): + return 1 + print("") + print(" 部署完成!运行目录: %s" % cfg["dist_path"]) + return 0 + + # deploy 模式 + cfg = get_cfg() + print("=" * 60) + print(" Soul 一键部署(直接覆盖)") + print("=" * 60) + print(" 服务器: %s@%s 项目路径: %s PM2: %s" % (cfg["user"], cfg["host"], cfg["project_path"], cfg["pm2_name"])) + print("=" * 60) + if not args.no_upload: + check_node_environments(cfg) + if not args.no_build: + print("[1/4] 本地构建 ...") + if not run_build(root): + return 1 + elif not os.path.isfile(os.path.join(root, ".next", "standalone", "server.js")): + print("[错误] 未找到 .next/standalone/server.js") + return 1 + else: + print("[1/4] 跳过本地构建") + tarball = pack_standalone_tar(root) + if not tarball: + return 1 + if not args.no_upload: + if not upload_and_extract(cfg, tarball): + return 1 + try: + os.remove(tarball) + except Exception: + pass + else: + print(" 压缩包: %s" % tarball) + if not args.no_api and not args.no_upload: + deploy_via_baota_api(cfg) + print("") + print(" 部署完成!站点: %s" % cfg["site_url"]) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/fix_port_issue.py b/scripts/fix_port_issue.py new file mode 100644 index 00000000..46ef777f --- /dev/null +++ b/scripts/fix_port_issue.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import paramiko +import time + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15) + +print("=== 1. 检查端口占用 ===") +cmd = "ss -tlnp | grep ':300' | head -10" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "无 300x 端口监听") + +print("\n=== 2. 检查 server.js 中的端口配置 ===") +cmd = "grep -n 'PORT\\|port\\|3006\\|30006' /www/wwwroot/soul/server.js | head -10" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +print("\n=== 3. 检查环境变量配置 ===") +cmd = "cat /www/wwwroot/soul/.env 2>/dev/null | grep -i port || echo '无 .env 文件'" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +print("\n=== 4. 停止 PM2 soul ===") +cmd = "pm2 stop soul 2>&1" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +time.sleep(2) + +print("\n=== 5. 杀死占用 3006 端口的进程 ===") +cmd = "lsof -ti:3006 | xargs kill -9 2>/dev/null || echo '无进程占用 3006'" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +print("\n=== 6. 杀死占用 30006 端口的进程 ===") +cmd = "lsof -ti:30006 | xargs kill -9 2>/dev/null || echo '无进程占用 30006'" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +time.sleep(1) + +print("\n=== 7. 确认端口已释放 ===") +cmd = "ss -tlnp | grep ':300'" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "[OK] 端口已全部释放") + +client.close() + +print("\n" + "=" * 60) +print("下一步:修复 server.js 的端口配置为 30006") diff --git a/scripts/restart_pm2.py b/scripts/restart_pm2.py new file mode 100644 index 00000000..5a3eb9d6 --- /dev/null +++ b/scripts/restart_pm2.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import paramiko +import time + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15) + +print("=== PM2 restart soul ===") +stdin, stdout, stderr = client.exec_command('pm2 restart soul 2>&1', timeout=30) +result = stdout.read().decode('utf-8', errors='replace') +# 移除可能导致编码问题的特殊字符 +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +print("\n=== 等待 3 秒 ===") +time.sleep(3) + +print("=== PM2 status ===") +stdin, stdout, stderr = client.exec_command('pm2 status soul 2>&1', timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +client.close() + +print("\n" + "=" * 50) +print("请在浏览器按 Ctrl+Shift+R 强制刷新页面!") diff --git a/scripts/restart_soul_correctly.py b/scripts/restart_soul_correctly.py new file mode 100644 index 00000000..83bb68bc --- /dev/null +++ b/scripts/restart_soul_correctly.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import paramiko +import time + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15) + +print("=== 1. 杀死所有相关进程 ===") +cmd = "kill -9 1822 2>/dev/null || echo 'Process 1822 already killed'" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +time.sleep(1) + +print("\n=== 2. 确认端口清理完成 ===") +cmd = "ss -tlnp | grep ':300' || echo '[OK] All ports cleared'" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +print("\n=== 3. 删除 PM2 soul 配置 ===") +cmd = "pm2 delete soul 2>&1" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +time.sleep(1) + +print("\n=== 4. 使用正确配置重新启动 ===") +cmd = """cd /www/wwwroot/soul && PORT=30006 pm2 start server.js --name soul --update-env 2>&1""" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +time.sleep(3) + +print("\n=== 5. 检查 PM2 状态 ===") +cmd = "pm2 status soul 2>&1" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +print("\n=== 6. 确认端口 30006 监听 ===") +cmd = "ss -tlnp | grep ':30006'" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result if result else "[X] Port 30006 not listening!") + +print("\n=== 7. 测试 HTTP 响应 ===") +cmd = "curl -I http://127.0.0.1:30006 2>&1 | head -5" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +print(result) + +print("\n=== 8. 查看最新日志 ===") +cmd = "pm2 logs soul --lines 10 --nostream 2>&1 | tail -15" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +print("\n=== 9. 保存 PM2 配置 ===") +cmd = "pm2 save 2>&1" +stdin, stdout, stderr = client.exec_command(cmd, timeout=10) +result = stdout.read().decode('utf-8', errors='replace') +result = result.encode('ascii', errors='replace').decode('ascii') +print(result) + +client.close() + +print("\n" + "=" * 60) +print("完成!请在浏览器访问: https://soul.quwanzhi.com") +print("如果仍是空白,按 Ctrl+Shift+R 强制刷新") diff --git a/scripts/start-standalone.js b/scripts/start-standalone.js index 6c923f72..6f57ea9e 100644 --- a/scripts/start-standalone.js +++ b/scripts/start-standalone.js @@ -55,34 +55,52 @@ if (!fs.existsSync(standaloneDir)) { process.exit(1); } -// 复制静态资源 +// 复制静态资源(缺一不可,否则部署到线上也会 404) console.log('📦 复制静态资源...'); +if (!fs.existsSync(staticSrc)) { + console.error('❌ 错误:.next/static 不存在,请先执行 pnpm build'); + process.exit(1); +} console.log(' .next/static → .next/standalone/.next/static'); copyDir(staticSrc, staticDst); +const chunksDir = path.join(staticDst, 'chunks'); +if (!fs.existsSync(chunksDir)) { + console.error('❌ 错误:复制后 .next/standalone/.next/static/chunks 不存在,本地会 404,部署线上也会报错'); + process.exit(1); +} + console.log(' public → .next/standalone/public'); copyDir(publicSrc, publicDst); -console.log('✅ 静态资源复制完成\n'); - -// 启动服务器 -const serverPath = path.join(standaloneDir, 'server.js'); -// 优先使用环境变量 PORT,未设置时提示并退出 -const port = process.env.PORT; - -if (!port) { - console.error('❌ 错误:未设置 PORT 环境变量'); - console.error(' 请设置端口后启动,例如:'); - console.error(' PORT=30006 pnpm start'); - console.error(' 或:'); - console.error(' export PORT=30006 && pnpm start'); - process.exit(1); +// 同步构建索引:BUILD_ID、build-manifest 等,避免服务器用错版本导致 404 +const nextRoot = path.join(rootDir, '.next'); +const nextStandalone = path.join(standaloneDir, '.next'); +const indexFiles = ['BUILD_ID', 'build-manifest.json', 'app-path-routes-manifest.json', 'routes-manifest.json']; +for (const name of indexFiles) { + const src = path.join(nextRoot, name); + const dst = path.join(nextStandalone, name); + if (fs.existsSync(src)) { + try { + fs.copyFileSync(src, dst); + } catch (e) { + console.warn(' [警告] 复制索引失败 %s: %s', name, e.message); + } + } } -console.log(`🌐 启动服务器: http://localhost:${port}`); -console.log(''); +console.log('✅ 静态资源与构建索引已同步\n'); -const server = spawn('node', [serverPath], { +// 启动服务器(必须在 standalone 目录下运行,否则 _next/static 会 404) +const serverPath = path.join(standaloneDir, 'server.js'); +const DEFAULT_PORT = 30006; +const port = process.env.PORT || String(DEFAULT_PORT); + +console.log(`🌐 启动服务器: http://localhost:${port}`); +console.log(' (静态资源已复制到 .next/standalone,请勿直接 cd 到 standalone 用 node server.js)\n'); + +const server = spawn('node', ['server.js'], { + cwd: standaloneDir, // 关键:工作目录必须是 standalone,否则找不到 .next/static stdio: 'inherit', env: { ...process.env, PORT: port } }); diff --git a/开发文档/8、部署/分销与绑定流程图.md b/开发文档/8、部署/分销与绑定流程图.md new file mode 100644 index 00000000..8fd78cee --- /dev/null +++ b/开发文档/8、部署/分销与绑定流程图.md @@ -0,0 +1,383 @@ +# 分销与绑定流程图 + +> 用流程图把「绑定」和「推荐人/邀请码」在系统中的用法讲清楚。 +> 建议配合《邀请码分销规则说明》一起看。 + +--- + +## 一、概念速查 + +| 名词 | 是什么 | 存哪儿 | 谁用 | +|------|--------|--------|------| +| **邀请码** | 一串码,如 `SOULABC123` | 每个用户一条:`users.referral_code` | 链接里 `ref=邀请码`,用来**认出**是谁推荐的 | +| **推荐人** | 拿佣金的那个人(用户) | 用**用户ID**存:`referrer_id` | 绑定表、订单表、分佣都只认这个 ID | +| **被推荐人** | 通过链接进来的访客/买家 | 用**用户ID**存:`referee_id` | 绑定表里「谁被谁推荐」 | + +关系:**邀请码** → 查 `users` 表 → 得到**推荐人用户ID**(referrer_id)。系统里所有「归属、分佣」只认 referrer_id,不直接认邀请码字符串。 + +--- + +## 二、整体流程总览(一图看懂) + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 分销全流程:从分享到分佣 │ +└─────────────────────────────────────────────────────────────────────────────────┘ + + 推广者 A(推荐人) 访客/买家 B(被推荐人) 系统 + + │ │ │ + │ 1. 分享带 ref 的链接 │ │ + │ ?ref=A的邀请码 │ │ + ├─────────────────────────────────────>│ 2. 点击链接进入小程序/阅读页 │ + │ ├─────────────────────────────────>│ + │ │ app.js: 存 referral_code │ + │ │ 可选: 记录访问 referral_visit │ + │ │ │ + │ │ 3. 登录(微信/手机号/getOpenId 拿到 user) │ + │ ├─────────────────────────────────>│ + │ │ 登录成功即调 /api/referral/bind │ + │ │ 入参: userId, referralCode │ + │ │ │ + │ │ 4. 绑定逻辑 │ + │ │ referral_code │ + │ │ → 查 users 得 │ + │ │ referrer_id=A │ + │ │ 写 referral_ │ + │ │ bindings │ + │ │ (referee=B, │ + │ │ referrer=A) │ + │ │<─────────────────────────────────┤ + │ │ 绑定成功(new/renew/takeover) │ + │ │ │ + │ │ 5. 下单(章节/找伙伴) │ + │ │ POST /api/miniprogram/pay │ + │ │ body: referralCode(可选) │ + │ ├─────────────────────────────────>│ + │ │ 6. 定推荐人 │ + │ │ 先查 bindings │ + │ │ (referee=B)→A │ + │ │ 无则用 referral│ + │ │ Code 解析→A │ + │ │ 写 orders. │ + │ │ referrer_id=A,│ + │ │ referral_code │ + │ │<─────────────────────────────────┤ + │ │ 返回支付参数 │ + │ │ │ + │ │ 7. 调起微信支付 │ + │ ├───────────────────────────────> 微信 + │ │ 8. 用户付款成功 │ + │ │<─────────────────────────────── 微信 + │ │ │ + │ │ 9. 支付回调 │ + │ │ POST .../notify│ + │ │ 查 bindings │ + │ │ (referee=B)→A │ + │ │ 佣金=金额×90% │ + │ │ A.pending_ │ + │ │ earnings += 佣金│ + │ │ binding→ │ + │ │ converted │ + │ │ │ + │ 10. 推广者 A 看到待结算收益 +90% │ │ + │<─────────────────────────────────────│ │ +``` + +--- + +## 三、绑定流程(邀请码 → 推荐关系) + +绑定解决的是:**「谁(B)是通过谁(A)的链接来的」**,并写入 `referral_bindings`。 + +**绑定规则(后端统一保证):** +- **不重复绑定**:被推荐人 B 已有**当前推荐人 A** 的有效绑定时,再次用 A 的邀请码调用 bind → **不新建记录**,只做**续期**(把过期时间再延长 30 天)。 +- **有时效**:每条绑定的有效期为 **30 天**(`expiry_date`);分佣、下单定推荐人时只认「未过期」的绑定。 +- **超时可重新绑定**:超过 30 天未续期的绑定视为过期;此时 B 再通过**其他人 C** 的链接进来并登录 → 允许绑定到 C(旧绑定标记过期,新绑定 C,即「抢夺」);若仍通过 A 的链接 → 续期 A 的绑定。 + +**绑定从登录就开始**:只要前端拿到 userId(登录成功),就立刻用当前的 `pendingReferralCode` 调 `/api/referral/bind`,不等到下单。后端根据上述规则决定是**新绑定 / 续期 / 抢夺 / 拒绝**。 + +```mermaid +flowchart TB + subgraph 入口 + A1["推广者 A 分享链接
带 ref=A的邀请码"] + A2["访客 B 点击链接进入"] + end + + subgraph 前端 + B1["app.js: 检测到 ref"] + B2["写入 storage: referral_code + pendingReferralCode"] + B3["若已登录 → 立即调 bind"] + B4["若未登录 → 等任意登录成功后再调 bind"] + B5["登录含: login / loginWithPhone / getOpenId 拿到 user 时"] + end + + subgraph 后端绑定API["POST /api/referral/bind"] + C1["入参: userId(B), referralCode"] + C2["用 referralCode 查 users 表 → 推荐人 A"] + C3["不能自己推荐自己"] + C4["查 B 是否已有有效绑定(active)"] + C5{"已有绑定?"} + C6["同一推荐人 A → 只续期,不重复绑定
expiry = 当前+30天"] + C7["不同人且已过期(>30天) → 可重新绑定
旧绑定过期,新绑定 A"] + C8["不同人且未过期 → 拒绝"] + C9["无绑定 / 续期 / 抢夺后 → 写 binding
referrer_id=A, referee_id=B, expiry=+30天"] + end + + A1 --> A2 --> B1 --> B2 + B2 --> B3 + B2 --> B4 + B4 --> B5 + B3 --> C1 + B5 --> C1 + C1 --> C2 --> C3 --> C4 --> C5 + C5 -->|无| C9 + C5 -->|有,同一人| C6 --> C9 + C5 -->|有,另一人已过期| C7 --> C9 + C5 -->|有,另一人未过期| C8 +``` + +要点: +- **绑定表**是「谁推荐了谁」的**唯一权威**;分佣只看这张表。 +- **已有绑定不重复**:同一推荐人再次绑只续期;**30 天**内不能换绑其他推荐人,超过 30 天可重新绑定(被新推荐人「抢夺」或原推荐人续期)。 +- **邀请码**只在「解析出推荐人是谁」时用,解析完得到的是 **referrer_id**(用户ID)。 + +--- + +## 四、下单时「推荐人」怎么定(写订单) + +创建订单时要把「这笔单算谁的推广」记在 `orders.referrer_id` 和 `orders.referral_code`。逻辑是:**先认绑定,再认邀请码**。 + +```mermaid +flowchart LR + subgraph 请求 + R1["POST /api/miniprogram/pay"] + R2["body: userId(B), referralCode(可选)"] + end + + subgraph 定推荐人 + S1["查 referral_bindings"] + S2["WHERE referee_id = B
AND status='active'
AND expiry_date > NOW()"] + S3{"查到有效绑定?"} + S4["referrer_id = 绑定里的 referrer_id"] + S5["referrer_id = 用 referralCode
查 users 得到的 id"] + S6["都无 → referrer_id = null"] + end + + subgraph 写订单 + T1["INSERT orders"] + T2["referrer_id = 上面得到的"] + T3["referral_code = 请求里的 referralCode
或推荐人当前 users.referral_code"] + end + + R1 --> R2 --> S1 --> S2 --> S3 + S3 -->|是| S4 + S3 -->|否,但有 referralCode| S5 + S3 -->|否且无| S6 + S4 --> T1 + S5 --> T1 + S6 --> T1 + T1 --> T2 --> T3 +``` + +结论: +- **有绑定** → 订单的推荐人 = 绑定里的推荐人(与下单时传不传 referralCode 无关)。 +- **无绑定但传了 referralCode** → 用邀请码解析出推荐人,写入订单。 +- 订单上的 **referrer_id** 用于后台展示、对账;**分佣不看订单**,只看绑定表。 + +--- + +## 五、分佣流程(支付成功后) + +分佣**只看绑定表**,不看订单上的 referrer_id。 + +```mermaid +flowchart TB + subgraph 触发 + P1["微信支付成功"] + P2["POST /api/miniprogram/pay/notify"] + P3["body: 订单号、金额、买家等"] + end + + subgraph 回调逻辑 + Q1["更新订单 status=paid"] + Q2["解锁用户权限(章节/全书)"] + Q3["查 referral_bindings"] + Q4["WHERE referee_id = 买家"] + Q5["AND status='active'"] + Q6["AND expiry_date > NOW()"] + Q7{"查到有效绑定?"} + Q8["取 referrer_id = 推广者 A"] + Q9["佣金 = 订单金额 × 90%"] + Q10["A.pending_earnings += 佣金"] + Q11["该绑定 status → converted"] + Q12["记录 commission_amount, order_id"] + Q13["不分佣"] + end + + P1 --> P2 --> P3 --> Q1 --> Q2 --> Q3 --> Q4 --> Q5 --> Q6 --> Q7 + Q7 -->|是| Q8 --> Q9 --> Q10 --> Q11 --> Q12 + Q7 -->|否| Q13 +``` + +要点: +- 分佣**只认** `referral_bindings` 里「买家 → 有效绑定 → 推荐人」。 +- 订单里的 referrer_id / referral_code **不参与**分佣计算,只用于统计和展示。 + +--- + +### 什么情况下能拿到佣金(推广者视角) + +满足下面**全部**条件时,你(推广者)才能拿到这笔订单的佣金: + +1. **对方是通过你的链接进来的** + 对方点击的链接里带有你的邀请码(如 `?ref=你的邀请码`),进入小程序后系统会记下推荐码,并在登录时用于绑定。 + +2. **对方已经绑定到你** + 对方完成登录后,系统成功调用了绑定接口(新绑定或续期),且当前存在一条「被推荐人 = 对方、推荐人 = 你」的绑定记录,且该绑定 **status = active**、**expiry_date > 当前时间**(在 30 天有效期内或已续期)。 + +3. **对方在绑定有效期内下单并支付成功** + 对方在上述有效期内发起了购买(章节或全书),并完成微信支付;支付成功后,微信会回调我们的接口。 + +4. **支付回调时仍能查到你的有效绑定** + 支付成功回调执行时,系统按「买家 = 对方」查 `referral_bindings`,能查到一条有效绑定且推荐人是你,才会把约 90% 的佣金计入你的待结算收益(pending_earnings),并把该绑定标记为已转化(converted)。 + +**简单记**:你的链接 → 对方进来并登录绑定到你 → 有效期内对方付款 → 你拿佣金。 + +--- + +### 章节分享这块的分销收益方式 + +章节页分享(读某一章时分享给好友/朋友圈)与首页、推广中心的分享**用同一套绑定与分佣规则**,只是落地页是「某一章」的阅读页。收益方式如下: + +1. **入口与绑定** + 你从阅读页分享出去的链接带 `ref=你的邀请码`(例如 `/pages/read/read?id=1.2&ref=你的邀请码`)。对方点进后进入**该章节**阅读页,系统记下推荐码;对方**登录**后即完成绑定(新绑定或续期)。绑定规则(30 天、不重复绑、超时可重绑)与其它分享入口一致。 + +2. **收益比例** + 订单实付金额的**约 90%** 给推广者(与全书、其它章节一致,由 `referral_config.distributorShare` 配置)。对方买的是**这一章、别的章还是全书**,都按该笔订单金额 × 90% 计算佣金。 + +3. **计佣次数(每个被推荐人只计一次)** + 系统在支付成功回调里会查「该买家」的**有效绑定**,有则给推荐人加佣金,并把这条绑定标记为**已转化(converted)**。 + 因此:**同一个被推荐人在绑定有效期内,只有其「第一笔」支付会给你分佣**;该用户之后再买其它章节或全书,**不再**重复给你分佣(绑定已用掉)。 + +4. **小结** + **章节分享的收益**:你分享章节链接(带 ref)→ 对方进来并登录绑定到你 → 对方在有效期内**第一次**支付(可以是这一章、别的章或全书)→ 你获得**该笔订单金额的约 90%**;该用户后续订单不再给你分佣。 + +--- + +## 六、推荐人 vs 邀请码(怎么用、不混用) + +```mermaid +flowchart LR + subgraph 入口 + L1["链接 ref=SOULABC123"] + end + + subgraph 解析 + L2["邀请码 = SOULABC123"] + L3["users WHERE referral_code = ?"] + L4["推荐人 = 该用户的 id"] + end + + subgraph 存储 + M1["referral_bindings.referrer_id"] + M2["orders.referrer_id"] + M3["分佣发给谁"] + end + + L1 --> L2 --> L3 --> L4 + L4 --> M1 + L4 --> M2 + L4 --> M3 + + style L2 fill:#f9f,stroke:#333 + style L4 fill:#9f9,stroke:#333 +``` + +- **邀请码**:只在「从链接/请求里认出是谁」这一步用,用完就解析成 **referrer_id**。 +- **推荐人**:所有「归属、分佣、统计」都只用 **referrer_id**,不会把邀请码字符串当推荐人存。 + +--- + +## 七、表与字段关系简图 + +```mermaid +erDiagram + users ||--o{ referral_bindings : "referrer_id" + users ||--o{ referral_bindings : "referee_id" + users { + string id PK + string referral_code "自己的邀请码" + } + referral_bindings { + string referrer_id "推荐人(谁拿佣金)" + string referee_id "被推荐人(买家)" + string status "active|converted|expired" + timestamp expiry_date + } + orders { + string user_id "买家" + string referrer_id "推荐人ID(展示/对账)" + string referral_code "下单时邀请码(展示)" + } + referral_bindings ||--o{ orders : "分佣时关联" +``` + +- **绑定**:`referrer_id` = 推荐人,`referee_id` = 被推荐人;分佣只看这张表。 +- **订单**:`referrer_id`、`referral_code` 只做展示和对账,不参与分佣计算。 + +--- + +## 八、逻辑漏洞与注意点 + +以下为与流程图、实现对照后容易出现的漏洞和设计注意点,便于排查与加固。 + +### 8.1 严重:支付回调中买家身份不能信任客户端 + +**问题**:支付回调(`/api/miniprogram/pay/notify`)里若**优先**使用请求体/attach 里的 `userId` 作为买家,则该 `userId` 来自**创建订单时客户端传入**的 `body.userId`。若被篡改(如传成他人 userId),会导致: +- 订单归属、解锁权限记到错误用户; +- 分佣按「错误买家」查绑定表,可能把佣金算到错误推荐人或不分佣。 + +**正确做法**:**买家身份必须以微信回调中的 `openId` 为准**(微信侧不可伪造),用 `openId` 查 `users` 得到 `buyerUserId`;attach 中的 `userId` 仅作辅助或校验,不一致时以 openId 解析结果为准。 + +**实现建议**:在 notify 中先 `buyerUserId = 由 openId 查 users 得到`;若查不到再回退到 attach.userId,并打日志告警。 + +--- + +### 8.2 设计缺口:先下单、后绑定会导致无分佣 + +**问题**:流程图要求「先绑定、再下单」分佣才生效。若用户通过 A 的链接进入但**未调用** `/api/referral/bind`(未登录就下单、或 bind 失败/漏调),下单时传了 `referralCode`,订单上会有 `referrer_id=A`,但**分佣只看绑定表**,此时无绑定 → 不会给 A 分佣。 + +**结论**:这是当前设计下的预期行为,不是 bug,但需要在产品/运营上保证「进入后尽快登录并完成绑定」,或在文档中明确写清:**只有存在有效绑定时支付成功才会分佣**。 + +--- + +### 8.3 重复回调与重复分佣 + +**现状**:微信可能对同一笔支付多次回调。当前实现: +- 订单状态已为 `paid` 时跳过订单更新; +- 分佣时只取 `status='active'` 的绑定,且分佣后将该绑定置为 `converted`,同一买家不会再有第二条 active 绑定参与分佣。 + +因此**不会重复加佣**。无需改流程图,实现已防护。 + +--- + +### 8.4 绑定表与 users.referred_by 双写 + +**现状**:绑定 API 在「新绑定」或「抢夺」时会写 `users.referred_by`,与 `referral_bindings` 双写;「续期」只更新绑定表,不改 `referred_by`。 +分佣、下单定推荐人**只读绑定表**;GET 查询「我的推荐人」等可能读 `users.referred_by`。只要绑定接口保证 new/takeover 时双写一致,则无逻辑漏洞。若以后有接口只改 `referred_by` 而不改绑定表,就会不一致,需避免。 + +--- + +### 8.5 小结 + +| 类型 | 说明 | +|------------|------| +| 必须修 | 支付回调中买家身份以 openId 解析为准,不信任 attach.userId。 | +| 文档/产品 | 明确「先绑定再下单才能分佣」;未绑定仅下单只记订单归属、不分佣。 | +| 已防护 | 重复回调不会导致重复分佣。 | +| 需长期一致 | 绑定表与 users.referred_by 在 new/takeover 时双写,避免单改其一。 | + +--- + +若要把某一段改成「按步骤」的纯文字版或拆成多张图,可以说明要哪一段(绑定 / 下单 / 分佣 / 概念)。 diff --git a/开发文档/8、部署/支付订单修复总结.md b/开发文档/8、部署/支付订单修复总结.md deleted file mode 100644 index a64dfcf4..00000000 --- a/开发文档/8、部署/支付订单修复总结.md +++ /dev/null @@ -1,399 +0,0 @@ -# 支付订单修复总结 - -**日期**: 2026-02-04 -**修复范围**: 小程序支付的完整流程 - ---- - -## ✅ 已完成的修复 - -### 1. 订单创建机制 -- ✅ 支付前先创建订单(`status='created'`) -- ✅ 订单立即插入 `orders` 表 -- ✅ 即使数据库插入失败,仍继续支付流程 -- ✅ 订单包含完整信息(用户ID、产品类型、金额等) - -**文件**: `app/api/miniprogram/pay/route.ts` - ---- - -### 2. 权限检查逻辑 -- ✅ 支付前查询真实购买记录(基于 `orders` 表) -- ✅ 调用 `/api/user/purchase-status` 接口 -- ✅ 避免重复购买相同产品 -- ✅ 更新本地缓存后再进行支付 - -**文件**: `miniprogram/pages/read/read.js` - ---- - -### 3. 支付回调处理 -- ✅ 更新订单状态为 `paid` -- ✅ 订单不存在时自动补记 -- ✅ 解锁用户权限(全书/章节) -- ✅ 分配推荐佣金(90%) -- ✅ **清理相同产品的其他未支付订单** - -**文件**: `app/api/miniprogram/pay/notify/route.ts` - ---- - -### 4. 多订单处理逻辑 -- ✅ 检查是否已有相同产品的已支付订单 -- ✅ 只在首次购买时解锁权限 -- ✅ 支付成功后删除其他未支付订单 -- ✅ 避免数据库中积累大量无效订单 - -**文件**: `app/api/miniprogram/pay/notify/route.ts` - ---- - -### 5. 购买状态刷新 -- ✅ 支付成功后自动刷新用户购买状态 -- ✅ 调用 `/api/user/purchase-status` 接口 -- ✅ 更新 `globalData` 和本地缓存 -- ✅ 确保下次访问不会被要求二次付费 - -**文件**: `miniprogram/pages/read/read.js` - ---- - -## 📊 完整流程 - -``` -┌─────────────────────────────────────────────────┐ -│ 用户点击购买按钮 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 1. 前端检查购买状态 │ -│ GET /api/user/purchase-status │ -│ ├─ 已购买 → 提示"已购买",停止 │ -│ └─ 未购买 → 继续 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 2. 前端创建支付订单 │ -│ POST /api/miniprogram/pay │ -│ ├─ ✅ 插入 orders 表 (status='created') │ -│ ├─ ✅ 检查是否已有已支付订单(记录日志) │ -│ ├─ 调用微信统一下单接口 │ -│ └─ 返回支付参数 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 3. 小程序调起微信支付 │ -│ wx.requestPayment(...) │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 4. 用户完成支付 / 取消支付 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 5. 微信支付回调(支付成功) │ -│ POST /api/miniprogram/pay/notify │ -│ ├─ ✅ 验证签名 │ -│ ├─ ✅ 更新订单 (status='paid') 或补记订单 │ -│ ├─ ✅ 解锁用户权限 │ -│ │ ├─ 全书 → users.has_full_book = TRUE │ -│ │ └─ 章节 → 添加到 purchased_sections │ -│ ├─ ✅ 分配推荐佣金(90%) │ -│ │ ├─ 更新 users.pending_earnings │ -│ │ └─ 更新 referral_bindings.status │ -│ └─ ✅ 清理相同产品的其他未支付订单 │ -│ └─ DELETE FROM orders WHERE status='created' │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 6. 前端刷新购买状态 │ -│ GET /api/user/purchase-status │ -│ ├─ 更新 globalData.purchasedSections │ -│ ├─ 更新本地缓存 │ -│ └─ 刷新页面,显示已购买内容 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ ✅ 完成 │ -└─────────────────────────────────────────────────┘ -``` - ---- - -## 📝 修改的文件清单 - -### 后端接口(5个) - -| 文件 | 类型 | 说明 | -|-----|------|------| -| `app/api/miniprogram/pay/route.ts` | 修改 | 添加订单插入逻辑 | -| `app/api/miniprogram/pay/notify/route.ts` | 修改 | 完善回调处理逻辑 | -| `app/api/user/purchase-status/route.ts` | 新建 | 查询购买状态 | -| `app/api/user/check-purchased/route.ts` | 新建 | 检查是否已购买 | -| `app/api/payment/create-order/route.ts` | 修改 | 通用支付接口(未使用) | - ---- - -### 前端逻辑(1个) - -| 文件 | 类型 | 说明 | -|-----|------|------| -| `miniprogram/pages/read/read.js` | 修改 | 购买检查 + 状态刷新 | - ---- - -### 文档(3个) - -| 文件 | 说明 | -|-----|------| -| `开发文档/8、部署/支付订单完整修复方案.md` | 完整修复方案 | -| `开发文档/8、部署/支付接口清单.md` | 所有接口清单 | -| `开发文档/8、部署/支付订单修复总结.md` | 本文档 | - ---- - -## 🎯 关键特性 - -### 1. 订单必创建 -无论支付是否成功,都会先创建订单记录: -- ✅ 便于统计转化率 -- ✅ 记录用户支付意图 -- ✅ 支持订单补记机制 - ---- - -### 2. 多订单智能处理 -如果用户多次点击支付: -- ✅ 每次都会创建新订单 -- ✅ 但只要有一单支付成功,就算已购买 -- ✅ 其他未支付订单会被自动删除 -- ✅ 不会重复扣款或重复解锁 - ---- - -### 3. 权限判断准确 -基于 `orders` 表的 `status='paid'` 记录: -- ✅ 不依赖本地缓存 -- ✅ 不会因为换设备/清缓存而丢失购买记录 -- ✅ 支持跨设备同步 - ---- - -### 4. 订单补记机制 -如果创建订单时失败: -- ✅ 不影响用户支付 -- ✅ 回调时自动补记订单 -- ✅ 确保每个支付都有记录 - ---- - -### 5. 自动清理无效订单 -支付成功后: -- ✅ 自动删除相同产品的其他未支付订单 -- ✅ 避免数据库中积累大量无效数据 -- ✅ 只保留已支付的订单 - ---- - -## 🔧 测试要点 - -### 1. 正常支付流程 -``` -1. 打开未购买的章节 -2. 点击"购买章节" -3. 完成微信支付 -4. 验证: - - orders 表有一条 status='paid' 的订单 - - 用户可以阅读该章节 - - 推荐人获得佣金(如果有) -``` - ---- - -### 2. 重复购买测试 -``` -1. 购买章节1-1并完成支付 -2. 再次打开章节1-1 -3. 点击"购买章节" -4. 验证: - - 提示"已购买过此章节" - - 未创建新订单 -``` - ---- - -### 3. 多次点击测试 -``` -1. 快速点击"购买章节"3次(不完成支付) -2. 验证 orders 表有3条 status='created' 的订单 -3. 第4次点击并完成支付 -4. 验证: - - orders 表只有1条 status='paid' 的订单 - - 前3条订单被删除 -``` - ---- - -### 4. 换设备测试 -``` -1. 设备A购买章节1-1并完成支付 -2. 设备B登录相同账号 -3. 打开章节1-1 -4. 验证: - - 无需购买,直接可阅读 - - 基于 orders 表的 status='paid' 判断 -``` - ---- - -### 5. 网络故障测试 -``` -1. 模拟创建订单时数据库失败 -2. 用户仍完成微信支付 -3. 验证: - - 回调时自动补记订单 - - 用户权限正常解锁 - - 推荐人正常获得佣金 -``` - ---- - -## ⚠️ 注意事项 - -### 1. 必须重启服务器 -所有修改需要重启 Next.js 服务器才能生效: -```bash -npm run dev -``` - ---- - -### 2. 订单号格式 -小程序订单号格式:`MP + 时间戳(14位) + 随机数(6位)` -- 示例:`MP20260204123456789012` -- 与通用支付模块不同 - ---- - -### 3. 支付回调异步性 -- 微信支付回调可能有1-3秒延迟 -- 前端支付成功后等待2秒再刷新状态 -- 如果回调失败,微信会重试多次 - ---- - -### 4. 佣金分配规则 -- 佣金比例:90% -- 只分配给有效绑定关系的推荐人 -- 绑定必须在30天有效期内 -- 佣金先进入 `pending_earnings`(待结算) - ---- - -### 5. 数据库事务 -目前未使用事务,建议后续优化: -- 订单创建 + 权限解锁 + 佣金分配应该在一个事务中 -- 避免部分成功、部分失败的情况 - ---- - -## 📊 数据统计 - -### 订单状态分布 -```sql -SELECT status, COUNT(*) as count, SUM(amount) as total_amount -FROM orders -GROUP BY status; -``` - -**预期结果**: -- `created`: 少量(等待支付或被清理) -- `paid`: 多数(真实订单) -- `expired`: 极少(超时未支付) - ---- - -### 用户购买统计 -```sql -SELECT - COUNT(DISTINCT user_id) as total_buyers, - COUNT(*) as total_orders, - SUM(amount) as total_revenue -FROM orders -WHERE status = 'paid'; -``` - ---- - -### 章节购买排行 -```sql -SELECT - product_id, - COUNT(*) as purchase_count, - SUM(amount) as revenue -FROM orders -WHERE status = 'paid' AND product_type = 'section' -GROUP BY product_id -ORDER BY purchase_count DESC -LIMIT 10; -``` - ---- - -## 🎉 总结 - -### ✅ 已实现的功能 - -1. ✅ 支付前创建订单 -2. ✅ 支付成功更新订单 -3. ✅ 基于真实订单判断购买状态 -4. ✅ 多订单智能处理 -5. ✅ 自动清理无效订单 -6. ✅ 订单补记机制 -7. ✅ 权限正确解锁 -8. ✅ 佣金自动分配 - ---- - -### 📈 改进效果 - -| 指标 | 修复前 | 修复后 | -|-----|--------|--------| -| 订单记录完整性 | ❌ 0% | ✅ 100% | -| 重复购买概率 | ⚠️ 高 | ✅ 0% | -| 无效订单积累 | ⚠️ 大量 | ✅ 自动清理 | -| 跨设备购买状态 | ❌ 不同步 | ✅ 同步 | -| 管理端数据准确性 | ❌ 0% | ✅ 100% | -| 佣金分配准确性 | ⚠️ 不稳定 | ✅ 稳定 | - ---- - -### 🚀 后续优化建议 - -1. **添加数据库事务** - - 订单创建 + 权限解锁 + 佣金分配 在一个事务中 - - 避免部分成功的情况 - -2. **添加订单过期定时任务** - - 定期清理超过30分钟的未支付订单 - - 将 status 更新为 `expired` - -3. **添加订单查询接口** - - 用户可以查看自己的订单历史 - - 支持订单详情、退款等功能 - -4. **优化支付体验** - - 支付前显示订单预览 - - 支付过程中显示进度 - - 支付失败时提供更详细的错误信息 - -5. **添加监控告警** - - 订单创建失败率 - - 支付回调失败率 - - 佣金分配失败率 - ---- - -**支付订单流程已完整修复!** 🎉 - -**现在可以正常使用小程序支付功能了!** diff --git a/开发文档/8、部署/支付订单未创建问题分析.md b/开发文档/8、部署/支付订单未创建问题分析.md deleted file mode 100644 index d906eb48..00000000 --- a/开发文档/8、部署/支付订单未创建问题分析.md +++ /dev/null @@ -1,488 +0,0 @@ -# 支付订单未创建问题完整分析 - -**日期**: 2026-02-04 -**问题**: 小程序支付成功但 orders 表没有订单记录 - ---- - -## 🔍 问题现象 - -1. ❌ 用户在小程序完成支付 -2. ❌ 查询 orders 表,没有任何记录 -3. ✅ 支付成功(微信回调可能已触发) -4. ❌ 用户权限未解锁 -5. ❌ 管理端数据为 0 - ---- - -## 🎯 根本原因 - -### **核心问题:数据库 ENUM 类型不匹配** - -**数据库表定义** (`lib/db.ts` 第155行): -```sql -status ENUM('pending', 'paid', 'cancelled', 'refunded') DEFAULT 'pending' -``` - -**代码中使用的值** (`app/api/miniprogram/pay/route.ts` 第177行): -```typescript -status: 'created' // ❌ 这个值不在 ENUM 中! -``` - -**MySQL 行为**: -- 当插入不在 ENUM 定义中的值时,MySQL 会: - - **严格模式**: 报错并拒绝插入 - - **非严格模式**: 插入空字符串或默认值 -- 无论哪种情况,数据都**无法正确插入** - ---- - -## 📊 完整支付流程分析 - -### 流程图 - -``` -┌─────────────────────────────────────────────────┐ -│ 1. 用户点击"购买章节" │ -│ miniprogram/pages/read/read.js (line 457) │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 2. 调用 processPayment() │ -│ - 检查是否已登录 │ -│ - 获取 openId │ -│ - 准备支付参数 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 3. 调用后端 POST /api/miniprogram/pay │ -│ 请求参数: │ -│ { │ -│ openId: "oXXXX...", │ -│ productType: "section", │ -│ productId: "1-1", │ -│ amount: 9.9, │ -│ description: "章节1-1...", │ -│ userId: "user_xxx" │ -│ } │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 4. 后端生成订单号 │ -│ orderSn = "MP20260204123456789012" │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 5. ❌ 尝试插入订单到数据库 │ -│ app/api/miniprogram/pay/route.ts (line 162) │ -│ │ -│ INSERT INTO orders ( │ -│ id, order_sn, user_id, open_id, │ -│ product_type, product_id, amount, │ -│ description, status, ... │ -│ ) VALUES ( │ -│ ..., 'created', ... ← ❌ ENUM不匹配 │ -│ ) │ -│ │ -│ 结果: 插入失败(被 try-catch 捕获) │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 6. 错误被捕获但不中断支付流程 │ -│ app/api/miniprogram/pay/route.ts (line 189) │ -│ │ -│ } catch (dbError) { │ -│ console.error('[MiniPay] ❌ 插入订单失败') │ -│ // 继续执行,不抛出异常 │ -│ } │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 7. ✅ 调用微信支付接口 │ -│ - 统一下单 API │ -│ - 获取 prepay_id │ -│ - 生成支付参数 │ -│ - 返回给小程序 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 8. ✅ 小程序调起微信支付 │ -│ wx.requestPayment(payParams) │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 9. ✅ 用户完成支付 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 10. 微信回调 POST /api/miniprogram/pay/notify │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 11. ❌ 回调处理失败 │ -│ - 查询订单 → ❌ 找不到(因为未创建) │ -│ - 尝试补记订单 → ❌ 仍然失败(ENUM不匹配) │ -│ - 无法解锁用户权限 │ -│ - 无法分配推荐佣金 │ -└──────────────┬──────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 结果: 用户支付了,但没有任何记录! │ -│ - orders 表: 空 │ -│ - users.purchased_sections: 未更新 │ -│ - users.pending_earnings: 未更新 │ -│ - referral_bindings: 未更新 │ -└─────────────────────────────────────────────────┘ -``` - ---- - -## 🐛 为什么错误没有暴露? - -### 1. 错误被静默捕获 - -```typescript -// app/api/miniprogram/pay/route.ts (line 189-193) -} catch (dbError) { - console.error('[MiniPay] ❌ 插入订单失败:', dbError) - // ⚠️ 错误被捕获,但不中断支付流程 - // 理由:微信支付成功后仍可以通过回调补记订单 -} -``` - -**设计意图**: 即使数据库插入失败,也让用户继续支付,回调时补记订单 - -**实际问题**: -- 回调时补记订单**同样会失败**(ENUM 不匹配) -- 用户支付了但系统没有任何记录 - ---- - -### 2. 日志可能不明显 - -```typescript -console.error('[MiniPay] ❌ 插入订单失败:', dbError) -``` - -**问题**: -- 日志可能被大量其他日志淹没 -- 如果没有监控告警,容易被忽略 -- 错误信息可能不够明确 - ---- - -### 3. 微信支付接口仍然成功 - -```typescript -// 调用微信统一下单接口 (line 195-204) -const response = await fetch('https://api.mch.weixin.qq.com/pay/unifiedorder', { - method: 'POST', - headers: { 'Content-Type': 'application/xml' }, - body: xmlData, -}) -``` - -**结果**: -- 微信支付接口返回成功 -- 小程序正常调起支付 -- 用户完成支付 -- **但系统内部没有订单记录** - ---- - -## 💡 为什么回调也无法补记? - -### 回调补记逻辑 - -```typescript -// app/api/miniprogram/pay/notify/route.ts (line 145-188) -try { - const orderRows = await query(` - SELECT id, user_id, product_type, product_id, status - FROM orders - WHERE order_sn = ? - `, [orderSn]) as any[] - - if (orderRows.length === 0) { - // ❌ 订单不存在,尝试补记 - console.warn('[PayNotify] ⚠️ 订单不存在,尝试补记:', orderSn) - - await query(` - INSERT INTO orders ( - id, order_sn, user_id, open_id, - product_type, product_id, amount, description, - status, transaction_id, pay_time, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - // ^^^^^ ❌ 仍然会失败(ENUM不匹配) - `, [/* ... */]) - } -} -``` - -**问题**: 补记时使用的是 `'paid'` 状态,这个值**在旧的 ENUM 定义中存在**,所以补记**可能会成功**! - -**但是**: 如果你的生产数据库还是旧的 ENUM 定义(没有 'created'),那么: -- 初始插入失败('created' 不在 ENUM 中) -- 回调补记**可能成功**('paid' 在 ENUM 中) - -**检查方法**: -```sql --- 查看是否有 status='paid' 的订单(但没有 status='created' 的) -SELECT status, COUNT(*) as count -FROM orders -GROUP BY status; -``` - ---- - -## 🔧 完整修复方案 - -### 第一步:修改数据库表结构(必须!) - -```sql --- 连接到生产数据库 -mysql -h sh-cynosdbmysql-grp-27q7yv6u.sql.tencentcdb.com \ - -P 28329 \ - -u soul \ - -p soulTest - --- 修改 status 字段 -ALTER TABLE orders -MODIFY COLUMN status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired') -DEFAULT 'created'; - --- 验证修改 -DESCRIBE orders; -``` - ---- - -### 第二步:重新部署代码(已完成) - -- ✅ `lib/db.ts` 已更新 -- ✅ 代码已重新部署 - ---- - -### 第三步:测试验证 - -```bash -# 1. 小程序测试购买 -# 2. 查询数据库 -SELECT * FROM orders ORDER BY created_at DESC LIMIT 5; - -# 3. 检查订单状态 -SELECT - order_sn, - status, - product_type, - product_id, - amount, - created_at, - pay_time -FROM orders -WHERE created_at > NOW() - INTERVAL 1 HOUR; -``` - ---- - -## 📊 数据库状态检查 - -### 检查当前 orders 表定义 - -```sql --- 查看表结构 -SHOW CREATE TABLE orders; - --- 查看 status 字段定义 -SELECT - COLUMN_NAME, - COLUMN_TYPE, - COLUMN_DEFAULT, - IS_NULLABLE -FROM INFORMATION_SCHEMA.COLUMNS -WHERE TABLE_SCHEMA = 'soulTest' - AND TABLE_NAME = 'orders' - AND COLUMN_NAME = 'status'; -``` - -**预期结果(修复后)**: -``` -COLUMN_TYPE: enum('created','pending','paid','cancelled','refunded','expired') -COLUMN_DEFAULT: 'created' -``` - -**如果仍是旧的**: -``` -COLUMN_TYPE: enum('pending','paid','cancelled','refunded') -COLUMN_DEFAULT: 'pending' -``` - ---- - -## 🚨 紧急检查清单 - -如果用户已经支付但没有记录,需要紧急检查: - -### 1. 检查微信支付商户平台 - -登录微信支付商户平台,查看: -- 是否有实际的交易记录? -- 交易金额是多少? -- 交易状态是否为"支付成功"? - -### 2. 检查服务器日志 - -```bash -# SSH 连接到服务器 -ssh root@42.194.232.22 -p 22022 - -# 查看 Node.js 日志 -pm2 logs soul --lines 100 - -# 搜索错误日志 -pm2 logs soul | grep "插入订单失败" -pm2 logs soul | grep "PayNotify" -``` - -### 3. 检查数据库 - -```sql --- 查看所有订单(包括可能补记成功的) -SELECT * FROM orders WHERE created_at > '2026-02-04 00:00:00'; - --- 查看用户购买记录 -SELECT - id, - nickname, - has_full_book, - purchased_sections, - pending_earnings -FROM users -WHERE id IN ( - SELECT DISTINCT user_id FROM orders WHERE status = 'paid' -); -``` - ---- - -## 💰 用户已支付但无记录怎么办? - -如果确认用户已支付但系统无记录: - -### 方案一:手动补记订单 - -```sql --- 从微信商户平台获取信息: --- - 商户订单号(order_sn) --- - 微信订单号(transaction_id) --- - 用户openId --- - 支付金额 --- - 支付时间 - -INSERT INTO orders ( - id, order_sn, user_id, open_id, - product_type, product_id, amount, description, - status, transaction_id, pay_time, created_at, updated_at -) VALUES ( - 'MP20260204123456789012', -- order_sn(从微信平台获取) - 'MP20260204123456789012', - 'user_xxx', -- 从用户表查询 - 'oXXXX...', -- 用户的openId - 'section', -- 或 'fullbook' - '1-1', -- 章节ID(询问用户) - 9.9, - '章节1-1购买', - 'paid', -- ✅ 使用 'paid'(已支付) - '4200002345678901234', -- 微信交易号(从微信平台获取) - '2026-02-04 12:34:56', -- 支付时间(从微信平台获取) - NOW(), - NOW() -); - --- 解锁用户权限 -UPDATE users -SET purchased_sections = JSON_ARRAY_APPEND( - COALESCE(purchased_sections, '[]'), - '$', - '1-1' -) -WHERE id = 'user_xxx' - AND NOT JSON_CONTAINS(COALESCE(purchased_sections, '[]'), '"1-1"'); - --- 如果有推荐人,分配佣金 -UPDATE users -SET pending_earnings = pending_earnings + (9.9 * 0.9) -WHERE id = ( - SELECT referred_by FROM users WHERE id = 'user_xxx' -); -``` - ---- - -### 方案二:为用户直接解锁权限 - -如果无法补记订单,至少要解锁用户的阅读权限: - -```sql --- 解锁章节 -UPDATE users -SET purchased_sections = JSON_ARRAY_APPEND( - COALESCE(purchased_sections, '[]'), - '$', - '1-1' -) -WHERE id = 'user_xxx'; - --- 或者直接给全书权限(作为补偿) -UPDATE users -SET has_full_book = TRUE -WHERE id = 'user_xxx'; -``` - ---- - -## 🎯 核心问题总结 - -| 问题 | 原因 | 影响 | 修复 | -|-----|------|------|------| -| 订单未创建 | status='created' 不在 ENUM 中 | 数据库插入失败 | 修改 ENUM 定义 | -| 错误被隐藏 | try-catch 捕获但不抛出 | 问题不明显 | 添加告警监控 | -| 支付仍成功 | 微信支付独立于订单创建 | 用户扣款但无权限 | 修复 ENUM | -| 回调可能成功 | 补记时用 'paid'(在 ENUM 中) | 部分订单有记录 | 统一状态流程 | - ---- - -## ✅ 修复后的正常流程 - -``` -用户购买 - ↓ -创建订单(status='created')✅ - ↓ -调用微信支付 ✅ - ↓ -用户完成支付 ✅ - ↓ -微信回调 ✅ - ↓ -更新订单(status='paid')✅ - ↓ -解锁用户权限 ✅ - ↓ -分配推荐佣金 ✅ - ↓ -清理无效订单 ✅ -``` - ---- - -## 🔗 相关文档 - -- [支付订单完整修复方案](./支付订单完整修复方案.md) -- [订单表状态字段修复说明](./订单表状态字段修复说明.md) -- [支付接口清单](./支付接口清单.md) - ---- - -**立即执行数据库修复SQL,问题即可解决!** 🎉 diff --git a/开发文档/8、部署/管理端静态资源404排查.md b/开发文档/8、部署/管理端静态资源404排查.md new file mode 100644 index 00000000..bf06382b --- /dev/null +++ b/开发文档/8、部署/管理端静态资源404排查.md @@ -0,0 +1,263 @@ +# 管理端 / 前台静态资源 404 排查 + +> 现象:打开管理后台(或前台)时控制台报错: +> `Failed to load resource: the server responded with a status of 404 ()` +> 涉及文件如:`6a98f5c6b2554ef3.js`、`turbopack-0d89ab930ad9d74d.js`、`xxx.css` 等。 + +--- + +## 部署前本地自检(避免本地有问题就部署导致线上报错) + +**每次部署前建议按下面做一遍,本地不 404 再部署:** + +1. 在项目根目录执行: + ```bash + pnpm build + pnpm start + ``` +2. 浏览器打开 `http://localhost:30006`(或你设置的 PORT),确认页面正常、控制台无 `_next/static` 的 404。 +3. 若有 404:先按本文下面的「开发环境」或「问题 5」处理,修好后再部署。 +4. 确认无误后再执行 `python scripts/devlop.py` 或你的部署命令。 + +若跳过自检,本地缺失 `.next/static` 或跑错目录,部署到线上后同样会 404。 + +--- + +## 一、先区分环境 + +| 报错里出现的文件名 | 说明 | +|--------------------|------| +| **turbopack-*.js** | **开发环境**(Next.js 16 默认用 Turbopack)。说明你是在跑 `pnpm dev`,且浏览器在请求开发服务器的 chunk。 | +| **只有一长串 hash 的 .js / .css**(如 `6a98f5c6b2554ef3.js`) | 可能是**开发**也可能是**生产**;生产里不会有 turbopack 这个名字。 | + +--- + +## 二、开发环境(本机 `pnpm dev`)出现 404 + +### 原因简述 + +- 开发服务器重启后,chunk 的**文件名(hash)会变**,但浏览器可能还在用**旧页面**里的 script 地址去请求,导致 404。 +- 或者访问的地址/端口不对(例如打开了生产地址,却期望加载本机 dev 的 chunk)。 + +### 处理步骤 + +1. **强刷、清缓存** + - Windows:`Ctrl + Shift + R` 或 `Ctrl + F5` + - Mac:`Cmd + Shift + R` + - 或:开发者工具 → Network → 勾选「Disable cache」后再刷新 + +2. **确认访问地址** + - 管理端:`http://localhost:30006/admin`(本项目 dev 端口为 30006) + - 不要用 `https://soul.quwanzhi.com/admin` 时还指望加载本机的 turbopack chunk + +3. **重启开发服务器** + ```bash + # 停掉当前 dev(Ctrl+C),再起 + pnpm dev + ``` + 然后再强刷一次 `http://localhost:30006/admin` + +4. **仍 404** + - 看浏览器里 404 的请求 URL:是相对路径 `/_next/static/...` 还是别的? + - 若是 `/_next/static/...` 且域名是 localhost:30006,说明请求没到本机 dev 服务器,检查是否有代理/ hosts 把请求指到了别的机器。 + +--- + +## 三、生产环境(线上 soul.quwanzhi.com)出现 404 - 快速修复 + +### ⚡ 最快修复方法(推荐) + +**在宝塔面板操作:** + +1. 登录宝塔面板 → **网站** → **soul.quwanzhi.com** +2. 点击 **设置** → **反向代理** +3. 检查是否有 **location /** 的配置 +4. 如果没有,点击 **添加反向代理**,配置如下: + +``` +代理名称:soul +目标URL:http://127.0.0.1:30006 +发送域名:$host +``` + +5. 保存后,点击 **配置文件**,确保配置类似这样: + +```nginx +location / { + proxy_pass http://127.0.0.1:30006; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; +} +``` + +6. 保存配置,点击 **重载配置** 或 **重启 Nginx** + +**关键点**:必须有 `location /`(整站反代),不能只有 `location /api`,否则 `/_next/static/...` 会 404。 + +--- + +## 三、生产环境(线上 soul.quwanzhi.com)出现 404 - 详细排查 + +### 原因简述 + +- 线上是 **standalone 部署**,HTML 由 Node 输出,**JS/CSS 分片**来自 `.next/static`。 +- 若 404 的是 `/_next/static/chunks/xxx.js` 或 `/_next/static/css/xxx.css`,多半是: + 1. 部署时 **没有把 `.next/static` 完整拷到服务器**,或 + 2. **Nginx 没有把 `/_next` 请求反代到 Node**,或 + 3. 部署后**用了旧 HTML 缓存**(HTML 里引用的 chunk 名已在新构建里不存在)。 + +### 处理步骤 + +1. **确认静态资源是否在服务器** + ```bash + # SSH 到服务器后 + ls -la /www/wwwroot/soul/.next/static/chunks/ + ls -la /www/wwwroot/soul/.next/static/css/ + ``` + - 若目录不存在或很少文件,说明部署时 **没有正确复制 `.next/static`**。 + +2. **部署脚本是否复制了 static** + - 本仓库:`scripts/start-standalone.js` 会在**本机**启动前把 `.next/static` 拷到 standalone 目录。 + - 若用 `scripts/devlop.py` 部署:确认脚本里会把 **`.next/static`** 一起打包/上传到服务器(例如打包进 zip 或单独 rsync),且解压后路径为 `项目根/.next/static`。 + - 若用宝塔 Node 项目:启动的是 `node server.js`,工作目录里必须包含 `.next/standalone` 及其中拷贝好的 `.next/static`(standalone 的 server 会从当前目录下的 `.next/static` 提供静态资源)。 + +3. **Nginx 必须把整站反代到 Node** + - 管理端和前台都是同一套 Next 应用,**所有路径**(含 `/_next/static/...`)都应反代到 Node,例如: + ```nginx + location / { + proxy_pass http://127.0.0.1:30006; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + ``` + - **不要**只配 `location /api` 反代而把 `/` 指到别的目录,否则 `/_next/static/...` 会 404。 + +4. **清缓存后再试** + - 浏览器强刷:`Ctrl+Shift+R` / `Cmd+Shift+R`。 + - 若用了 CDN 或 Nginx 缓存,需 purge 或暂时关缓存,避免旧 HTML 引用已不存在的 chunk。 + +--- + +## 四、小结 + +| 环境 | 优先检查 | +|------|----------| +| **开发** | 强刷 / 清缓存;确认访问 `http://localhost:30006/admin`;重启 `pnpm dev` | +| **生产** | 服务器上是否存在 `.next/static`;Nginx 是否整站反代到 Node;浏览器/CDN 缓存是否导致旧 HTML | + +--- + +## 五、快速检查脚本(推荐) + +### 检查服务器状态 + +```bash +python scripts/check_static_files.py +``` + +### 检查 Nginx 配置 + +```bash +python scripts/fix_nginx_static.py +``` + +脚本会检查 Nginx 配置是否正确,并给出修复建议。 + +本仓库提供了检查脚本,可快速查看服务器上静态资源是否存在: + +```bash +python scripts/check_static_files.py +``` + +脚本会检查: +- `.next/static` 是否存在 +- PM2 项目的工作目录 +- Nginx 配置 +- 端口 30006 是否在监听 + +根据输出结果,可以快速定位问题。 + +--- + +## 六、常见问题与修复 + +### 问题 1:部署后 `.next/static` 不存在 + +**原因**:部署脚本在打包时复制了 static,但解压后路径不对或文件丢失。 + +**修复**: +1. SSH 到服务器检查:`ls -la /www/wwwroot/soul/.next/static/` +2. 若不存在,重新部署:`python scripts/devlop.py` +3. 部署后再次检查:`python scripts/check_static_files.py` + +### 问题 2:Nginx 只反代了 `/api`,没有反代 `/_next` + +**现象**:HTML 能加载,但所有 `/_next/static/...` 的请求都 404。 + +**修复**:Nginx 配置需要**整站反代**,例如: + +```nginx +location / { + proxy_pass http://127.0.0.1:30006; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +**不要**只配 `location /api`,否则 `/_next` 路径会 404。 + +### 问题 3:PM2 工作目录不对 + +**现象**:`server.js` 存在,但运行时找不到 `.next/static`(因为工作目录不是项目根目录)。 + +**修复**: +- 宝塔 PM2 管理器:确保「工作目录」填 `/www/wwwroot/soul`(项目根目录,不是 `.next/standalone`) +- 命令行 PM2:`pm2 start server.js --name soul --cwd /www/wwwroot/soul` + +### 问题 4:standalone 部署时 static 路径问题 + +**说明**:standalone 模式下,`server.js` 会从**当前工作目录**下的 `.next/static` 提供静态资源。 + +- ✅ 正确:工作目录 = 项目根(如 `/www/wwwroot/soul`),且该目录下有 `server.js`、`.next/static`、`public` +- ❌ 错误:工作目录 = `/www/wwwroot/soul/.next/standalone`,而 static 在项目根下(server.js 会从 cwd 找 `.next/static`,找不到就 404) + +**宝塔用 `node server.js` 没问题**:本仓库的 `scripts/devlop.py`(及 deploy 打包)会把 **server.js、.next/static、public** 一起打进同一层目录,解压到服务器后,项目路径里就同时有这三者;宝塔 Node 项目填「项目路径」= 该目录、启动命令 = `node server.js` 即可,无需改成别的。 + +**修复**:确保宝塔里「项目路径」是**解压后的项目根目录**(包含 server.js 和 .next/static 的那一层),不要填成 `.next/standalone` 子目录。 + +### 问题 5:本地打包后用 node 运行出现 404(woff2 / css / js / turbopack 等) + +**现象**:`pnpm build` 后直接 `cd .next/standalone && node server.js`,浏览器访问 localhost 报 404(如 `797e433ab948586e-s.p.xxx.woff2`、`turbopack-xxx.js`、各种 chunk 的 css/js)。 + +**原因**: +1. standalone 目录里**没有** `.next/static` 和 `public`,需要先复制进去。 +2. 若用**项目根**执行 `node .next/standalone/server.js` 且未复制 static 到 standalone,服务器会从项目根找 `.next/static`(有则可用);若在 standalone 目录下执行且未复制,则 404。 +3. 若 404 里出现 **turbopack-*.js**,多半是浏览器缓存了之前 **开发环境**的 HTML,强刷即可。 + +**修复**: +1. **不要**直接 `cd .next/standalone && node server.js`。应**在项目根**执行: + ```bash + pnpm start + ``` + 或: + ```bash + node scripts/start-standalone.js + ``` + 脚本会先把 `.next/static` 和 `public` 复制到 `.next/standalone`,并以正确工作目录启动,避免 404。 +2. 浏览器**强刷**:`Ctrl+Shift+R` / `Cmd+Shift+R`,避免旧 HTML 引用 turbopack 或已失效的 chunk。 + +--- + +按上述步骤仍 404 时,可把**具体 404 的完整 URL** 和**当前是 dev 还是线上**发出来,便于继续排查。 diff --git a/开发文档/8、部署/邀请码分销规则说明.md b/开发文档/8、部署/邀请码分销规则说明.md index 0d54bd4b..df788156 100644 --- a/开发文档/8、部署/邀请码分销规则说明.md +++ b/开发文档/8、部署/邀请码分销规则说明.md @@ -3,6 +3,8 @@ **配置来源**: 数据库 `system_config.config_key = 'referral_config'` **分佣逻辑**: `app/api/miniprogram/pay/notify/route.ts` 中 `processReferralCommission` +> 📌 **流程图**:绑定与分销的完整流程(谁推荐谁、下单怎么写推荐人、分佣怎么算)见 → [分销与绑定流程图](./分销与绑定流程图.md) + --- ## 一、分销规则(当前实现) @@ -37,15 +39,17 @@ ### 处理方式(已实现) 1. **orders 表增加字段** - `referrer_id`(VARCHAR(50) NULL):下单时若存在有效绑定或邀请码,则写入推荐人 user_id。 - - **迁移脚本**:`python scripts/add_orders_referrer_id.py`(首次部署或表已存在时执行一次)。 + - `referral_code`(VARCHAR(20) NULL):**下单时使用的邀请码**,直接记录在订单上便于对账与后台展示。 + - **迁移脚本**:`python scripts/add_orders_referrer_id.py`、`python scripts/add_orders_referral_code.py`(表已存在时各执行一次)。 -2. **下单时写推荐人** +2. **下单时写推荐人与邀请码** - 创建订单时先按**买家 user_id** 查 `referral_bindings`(referee_id = 买家、有效且未过期),取 `referrer_id`。 - 若未查到且请求体带了 `referralCode`,则用 `users.referral_code = referralCode` 解析出推荐人 id,写入 `orders.referrer_id`。 - - 以服务端绑定为主,邀请码为补充。 + - **邀请码**:优先存请求体里的 `referralCode`(用户章节支付时传的);若未传但已有 `referrer_id`,则存该推荐人当前的 `users.referral_code`,保证订单上有一份当时使用的邀请码记录。 3. **小程序传参** - - 支付请求会传 `referralCode`:来自 `wx.getStorageSync('referral_code')`(落地页 ref 带入的“谁邀请了我”的邀请码),供后端在无绑定时解析推荐人。 + - 支付请求会传 `referralCode`:来自 `wx.getStorageSync('referral_code')`(落地页 ref 带入的“谁邀请了我”的邀请码),供后端解析推荐人并写入 `orders.referrer_id` 与 `orders.referral_code`。 + - **同步约定**:`app.js` 在检测到 `ref` / `referralCode` 时除写入 `pendingReferralCode` 外,会同步写入 `referral_code`;**章节支付**(`pages/read/read.js`)与**找伙伴支付**(`pages/match/match.js`)创建订单时都会带上 `referralCode`,保证两类订单都会记录邀请码。 --- @@ -65,7 +69,7 @@ |-----------|------| | **users** | referral_code(自己的邀请码), referred_by(可选), pending_earnings, earnings | | **referral_bindings** | referrer_id, referee_id, status(active/converted/expired), expiry_date, commission_amount, order_id | -| **orders** | referrer_id(推荐人用户ID,下单时写入,用于分销归属与统计) | +| **orders** | referrer_id(推荐人用户ID), referral_code(下单时使用的邀请码,便于对账与展示) | | **system_config** | config_key = 'referral_config',含 distributorShare、bindingDays 等 | --- @@ -77,3 +81,67 @@ 3. 支付成功回调:`/api/miniprogram/pay/notify` 再根据 B 查绑定,给 A 结算 90% 佣金,更新 `referral_bindings` 与 `users.pending_earnings`。 这样订单上就有邀请/分销关系(referrer_id),且分佣规则不变。 + +--- + +## 六、推荐人 vs 邀请码:会不会乱? + +**结论:不会乱。** 全局只认「推荐人 = 用户ID」,邀请码只用于解析出这个 ID。 + +### 概念区分 + +| 概念 | 含义 | 存储位置 | 用途 | +|------|------|----------|------| +| **邀请码** | 一串码(如 SOULABC123) | `users.referral_code`(每个用户一条) | 链接里带 `ref=邀请码`,用来**识别**是谁推荐的 | +| **推荐人** | 拿佣金的那个人 | 用**用户ID** `referrer_id` 存 | 订单归属、分佣、统计都只认 ID,不认字符串 | + +- 邀请码 → 通过 `users WHERE referral_code = ?` 可唯一解析出 → **推荐人用户ID**。 +- 订单表、绑定表里存的都是 **referrer_id**,从不存邀请码字符串;展示时再用 referrer_id 去查昵称/邀请码即可。 + +### 绑定与订单归属的优先级(唯一权威) + +1. **下单时**(`/api/miniprogram/pay`) + - **先**查 `referral_bindings`:当前买家是否有有效绑定 → 得到 `referrer_id`。 + - **仅当没有绑定**时,才用请求体里的 `referralCode` 去 `users` 表解析出 `referrer_id`。 + - 最终写入订单的**只有** `orders.referrer_id`(用户ID),不会写邀请码。 + +2. **分佣时**(支付成功回调) + - 只查 `referral_bindings`(买家 → 有效绑定的推荐人),**不看**订单上的 referrer_id,也不看邀请码。 + - 佣金发给绑定表里的 `referrer_id`。 + +因此: +- **绑定表** = 权威的「谁推荐了谁」; +- **订单上的 referrer_id** = 下单时根据「绑定表 + 兜底邀请码」算出来的结果,只用于展示/对账; +- **邀请码** = 仅作为入口参数,解析成 referrer_id 后就不再参与逻辑,不会和推荐人 ID 混用。 + +### 前端 storage 说明(避免混用) + +- 落地页/分享带 `ref`:写入 `referral_code`(下划线),支付时读 `referral_code` 传给后端作兜底。 +- App 层待绑定:`pendingReferralCode`;绑定成功后可选写 `boundReferralCode`。 +- 绑定接口、支付接口请求体里统一用 **referralCode**(驼峰)。 + +只要后端始终用「绑定表优先、邀请码兜底」且只落库 referrer_id,全局绑定逻辑就不会乱。 + +--- + +## 七、文章/章节分销 + +**结论:和全局分销是同一套逻辑,没有单独的「按文章维度」分销。** + +### 当前实现 + +- **分享首页**:链接形如 `https://xxx/?ref=邀请码`,点击后 ref 写入 storage,绑定与订单归属按上文规则。 +- **分享某篇文章/章节**: + - 小程序:`/pages/read/read?id=章节ID&ref=邀请码`(阅读页 `onShareAppMessage` / `onShareTimeline` 会带上当前用户邀请码)。 + - Web:`/view/read/章节ID?ref=邀请码`。 +- 访客从**任意**带 ref 的链接进入(首页或某篇文章),都会: + 1. 用 ref 解析出推荐人并完成绑定(`referral_bindings`); + 2. 之后该用户下单,订单归属与分佣都按**同一套**「绑定表优先、邀请码兜底」规则,与**从哪篇文章点进来**无关。 + +也就是说:**文章/章节只决定落地页内容,不改变绑定与分佣规则**。谁发的链接(ref=谁),谁就是推荐人;买的是哪一章、哪本书,都按 90% 给该推荐人,没有「这篇文章单独分成」或「按章节统计推广效果」的单独逻辑。 + +### 未实现的部分(若以后要做) + +- **按文章/章节维度的统计**:例如「通过《1.2 某某章》链接带来的访问/绑定/订单数」—— 当前未记录分享时的章节 id,无法区分。 +- **按文章的分成策略**:例如某章单独 95%、其他 90% —— 当前未实现,所有订单统一 90%。 +- 若需要「文章分销」统计或差异化分成,需要:在访问/绑定/订单上记录「来源章节」(如 `landing_section_id`),并在分佣或报表里按章节维度汇总。 diff --git a/开发文档/8、部署/部署脚本备份/README.md b/开发文档/8、部署/部署脚本备份/README.md deleted file mode 100644 index 2306449b..00000000 --- a/开发文档/8、部署/部署脚本备份/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 部署脚本备份 - -本目录存放 **scripts/devlop.py** 的备份副本,仅作存档与应急恢复用。 - -- **日常部署**:请在项目根目录执行 `python scripts/devlop.py`。 -- **备份说明**:备份内容与 `scripts/devlop.py` 逻辑一致;若脚本有更新,可在此目录同步更新本备份。 -- **关联文档**:`DEPLOYMENT.md`、`开发文档/8、部署/宝塔配置检查说明.md`、`开发文档/8、部署/当前项目部署到线上.md`。 diff --git a/开发文档/8、部署/部署脚本备份/devlop.py b/开发文档/8、部署/部署脚本备份/devlop.py deleted file mode 100644 index a3508d72..00000000 --- a/开发文档/8、部署/部署脚本备份/devlop.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Soul 创业派对 - 本地打包 + SSH 上传 + 宝塔 API 部署(备份) - -本文件为 scripts/devlop.py 的备份,仅作存档。日常部署请使用项目根目录下: - python scripts/devlop.py - -备份时间说明见同目录 README.md -""" -# 以下为 scripts/devlop.py 的完整内容备份 -# ========== 备份开始 ========== - -from __future__ import print_function - -import os -import sys -import shutil -import tarfile -import tempfile -import subprocess -import argparse - -ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) # 备份版:从 开发文档/8、部署/部署脚本备份 回项目根 - -try: - import paramiko -except ImportError: - print("请先安装: pip install paramiko") - sys.exit(1) - -try: - import requests - import urllib3 - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -except ImportError: - print("请先安装: pip install requests") - sys.exit(1) - -scripts_dir = os.path.join(ROOT, "scripts") -sys.path.insert(0, scripts_dir) -from deploy_baota_pure_api import CFG as BAOTA_CFG, restart_node_project - - -def get_cfg(): - return { - "host": os.environ.get("DEPLOY_HOST", "42.194.232.22"), - "user": os.environ.get("DEPLOY_USER", "root"), - "password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"), - "ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""), - "project_path": os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul"), - "app_port": os.environ.get("DEPLOY_APP_PORT", "30006"), - "pm2_name": os.environ.get("DEPLOY_PM2_APP", BAOTA_CFG["pm2_name"]), - } - - -def run_build(root): - print("[1/4] 本地构建 pnpm build ...") - use_shell = sys.platform == "win32" - r = subprocess.run(["pnpm", "build"], cwd=root, shell=use_shell, timeout=300) - if r.returncode != 0: - print("构建失败,退出码:", r.returncode) - return False - standalone = os.path.join(root, ".next", "standalone") - if not os.path.isdir(standalone) or not os.path.isfile(os.path.join(standalone, "server.js")): - print("未找到 .next/standalone 或 server.js,请确认 next.config 中 output: 'standalone'") - return False - print(" 构建完成.") - return True - - -def pack_standalone(root): - print("[2/4] 打包 standalone ...") - standalone = os.path.join(root, ".next", "standalone") - static_src = os.path.join(root, ".next", "static") - public_src = os.path.join(root, "public") - ecosystem_src = os.path.join(root, "ecosystem.config.cjs") - staging = tempfile.mkdtemp(prefix="soul_deploy_") - try: - for name in os.listdir(standalone): - src = os.path.join(standalone, name) - dst = os.path.join(staging, name) - if os.path.isdir(src): - shutil.copytree(src, dst) - else: - shutil.copy2(src, dst) - static_dst = os.path.join(staging, ".next", "static") - shutil.rmtree(static_dst, ignore_errors=True) - os.makedirs(os.path.dirname(static_dst), exist_ok=True) - shutil.copytree(static_src, static_dst) - public_dst = os.path.join(staging, "public") - shutil.rmtree(public_dst, ignore_errors=True) - shutil.copytree(public_src, public_dst) - shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs")) - tarball = os.path.join(tempfile.gettempdir(), "soul_deploy.tar.gz") - with tarfile.open(tarball, "w:gz") as tf: - for name in os.listdir(staging): - tf.add(os.path.join(staging, name), arcname=name) - print(" 打包完成: %s" % tarball) - return tarball - finally: - shutil.rmtree(staging, ignore_errors=True) - - -def upload_and_extract(cfg, tarball_path): - print("[3/4] SSH 上传并解压 ...") - host, user, password, key_path = cfg["host"], cfg["user"], cfg["password"], cfg["ssh_key"] - project_path = cfg["project_path"] - if not password and not key_path: - print("请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY") - return False - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - if key_path: - client.connect(host, username=user, key_filename=key_path, timeout=15) - else: - client.connect(host, username=user, password=password, timeout=15) - sftp = client.open_sftp() - remote_tar = "/tmp/soul_deploy.tar.gz" - sftp.put(tarball_path, remote_tar) - sftp.close() - cmd = ( - "cd %s && " - "rm -rf .next server.js node_modules public ecosystem.config.cjs 2>/dev/null; " - "tar -xzf %s && rm -f %s" - ) % (project_path, remote_tar, remote_tar) - stdin, stdout, stderr = client.exec_command(cmd, timeout=60) - err = stderr.read().decode("utf-8", errors="replace").strip() - if err: - print(" 服务器 stderr:", err) - if stdout.channel.recv_exit_status() != 0: - return False - print(" 上传并解压完成: %s" % project_path) - return True - except Exception as e: - print(" SSH 错误:", e) - return False - finally: - client.close() - - -def deploy_via_baota_api(cfg): - print("[4/4] 宝塔 API 重启 Node 项目 ...") - ok = restart_node_project(BAOTA_CFG["panel_url"], BAOTA_CFG["api_key"], cfg["pm2_name"]) - if not ok: - print("提示:若 Node 接口不可用,请在宝塔面板【Node 项目】中手动重启 %s" % cfg["pm2_name"]) - return ok - - -def main(): - parser = argparse.ArgumentParser(description="本地打包 + SSH 上传 + 宝塔 API 部署") - parser.add_argument("--no-build", action="store_true", help="跳过本地构建") - parser.add_argument("--no-upload", action="store_true", help="跳过 SSH 上传") - parser.add_argument("--no-api", action="store_true", help="上传后不调宝塔 API 重启") - args = parser.parse_args() - cfg = get_cfg() - print("=" * 60) - print(" Soul 创业派对 - 本地打包 + SSH 上传 + 宝塔 API 部署") - print("=" * 60) - print(" 服务器: %s@%s | 路径: %s | PM2: %s" % (cfg["user"], cfg["host"], cfg["project_path"], cfg["pm2_name"])) - print("=" * 60) - if not args.no_build: - if not run_build(ROOT): - return 1 - else: - if not os.path.isfile(os.path.join(ROOT, ".next", "standalone", "server.js")): - print("跳过构建但未找到 .next/standalone/server.js") - return 1 - tarball_path = pack_standalone(ROOT) - if not tarball_path: - return 1 - if not args.no_upload: - if not upload_and_extract(cfg, tarball_path): - return 1 - if os.path.isfile(tarball_path): - try: - os.remove(tarball_path) - except Exception: - pass - if not args.no_api and not args.no_upload: - deploy_via_baota_api(cfg) - print("") - print(" 站点: %s | 后台: %s/admin" % (BAOTA_CFG["site_url"], BAOTA_CFG["site_url"])) - print("=" * 60) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) - -# ========== 备份结束 ========== diff --git a/开发文档/9、手册/提示词/落地方案提示词.md b/开发文档/9、手册/提示词/落地方案提示词.md deleted file mode 100644 index a9233fae..00000000 --- a/开发文档/9、手册/提示词/落地方案提示词.md +++ /dev/null @@ -1,5 +0,0 @@ -# 落地方案提示词(占位) - -用于记录“把需求落到代码/流程”的提示词。 - -当你后面需要我按固定模板输出落地方案,就把模板写在这里。 diff --git a/开发文档/9、手册/提示词/说明手册提示词.md b/开发文档/9、手册/提示词/说明手册提示词.md deleted file mode 100644 index 99f74174..00000000 --- a/开发文档/9、手册/提示词/说明手册提示词.md +++ /dev/null @@ -1,5 +0,0 @@ -# 说明手册提示词(占位) - -用于记录“对外说明/交付手册”的提示词。 - -当你后面需要我按固定模板输出说明手册,就把模板写在这里。 diff --git a/开发文档/产研团队 第21场 20260129 许永平.txt b/开发文档/产研团队 第21场 20260129 许永平.txt deleted file mode 100644 index b2bba7a3..00000000 --- a/开发文档/产研团队 第21场 20260129 许永平.txt +++ /dev/null @@ -1,3738 +0,0 @@ -2026年1月29日 下午 5:17|2小时 6分钟 10秒 - -关键词: -付款、标签、微信、数据库、程序、吸收、飞书、后台、分销、虚拟环境、数据中心、用户管理、用户绑定、流量自动化、用户中心、数据统计、用户数据、文档管理 - -文字记录: -许永平 00:02 -录制吗?哦,可以了,都有加入会议签到一下,记得签,就我没签,我今天这边还是在做那个。上面投屏一下。对,这是昨天的不好操作。 - -许永平 00:34 -从第二点开始。第二点,数据中心加权限管理。上面的也总结哈。就除了。 MBTI 版 30 分钟 API 开放管。最新版的可以看一下位置在哪? SDK 了,我这边的话就权限管理,这是昨天的。昨天的。你们要,嗯,让老王那个你手机也要加入一下。哪一个? - -卡若 01:00 -会议,这样才能看到你的内容。 - -许永平 01:00 -行。 - -卡若 01:03 -你加他说话的时候,到时候会更。 - -许永平 01:03 -你加他之后会更。 - -卡若 01:06 -会更容易一次给你加字幕。 - -许永平 01:06 -会更好回忆。会给你加字幕,你可以声音关小了,但是有录就好,我这边可以把声音都关掉,反正我们都听得见。 - -卡若 01:08 -那你可以声音关小,但是有录音就行,我这边可以把声音都关掉,那我来听一听。 - -许永平 01:17 -行,我加一下会议。 - -卡若 01:22 -这是昨天的,昨天我这边数据中心加权限了,那一块开放的,是开放的,然后这边有一个提现的流程。 - -许永平 01:22 -这是昨天的,昨天我这边数据中心加权限管控那一块开放的,是开放完了,然后这边有一个提现流程的。以及 MBTI 30 分钟重要重构的一些逻辑,还有碎片化剪辑加时间。 - -卡若 01:33 -以及 NPS 30 分钟重会重构的一些逻辑,而序列化统一在时间最后的话是这个材料的吸收就是有一些用法,一些AI,一个是AI,大概这是昨天的内容,今天的话我这边就已经投入到这个接入 AI 模型这里处理了。 - -许永平 01:43 -最后的话是这个材料吸收就是有一些用法,一些就是一些 AI 体词的一些用法,挺好的,大概这是昨天的内容,今天的话我这边就已经投入到这个接入 AI 模型这里。处理了,我之前停下来了,现在就是要把它做了,做的再做一些预设的东西。 - -卡若 02:04 -之前停下来的,现在就是要把它做了,做的再做一些预设的东西,接下来,然后有一个问题。 - -许永平 02:11 -接下来,然后有一个问题是我那个本地的模型好像有点智障了。 - -卡若 02:14 -是我那个本地的模型好像有点智障啊。你跑本地的肯定是这样啊。 - -许永平 02:18 -跑本地。 - -卡若 02:19 -那我现在有很多。 - -许永平 02:19 -那我现在有没有比较好的代替哦? - -卡若 02:21 -你为什么要跑本地模型呢?我问问,就是昨天我问过那个LLAMA,我觉得这个模型去处理一些什么采集的,应该不需要太高级的模型吧? - -许永平 02:24 -问问。就是昨天有问过那个LLAMA,我觉得你这个模型去处理一些什么采集的,应该不需要太高级的模。 - -卡若 02:33 -哼,你这。 - -许永平 02:34 -还是说。 - -卡若 02:34 -还是说。本地的不好用,本地的不是这么用的,这个再说吧,你先过一下嘛。 - -许永平 02:39 -就是再说吧。 - -卡若 02:41 -行,然后其他的是老王这边,其他这些本来要读图书里的平台。 - -许永平 02:42 -行,然后其他的事,老王这边其他这些本来要弄读书,你那边不要再讨论。 - -卡若 02:48 -就那接口那些东西弄得咋样?就像我刚刚说的那几个东西。你。 - -许永平 02:54 -你。 - -卡若 02:55 -对啊。 - -许永平 02:55 -对啊。 - -卡若 02:56 -截,早上截图那个,是吧? - -许永平 02:56 -早截,早上截图那个,是吧? - -卡若 02:58 -是。就我刚发群里的那些,你得。 - -许永平 02:59 -是。 - -卡若 03:00 -底下一个标签计划,不算标签的计划。 - -许永平 03:01 -你那个标签。哦,那个我得,我还得,再,我还得写现在这个他是跑全部的,那我要写是独立。 - -卡若 03:04 -你得有接口过来啊。那个我得,我还得,再,我还得写现在这个他是跑全部的。不是,就是我现在小程序弄完了。对吧?那我现在需要接口给我。 - -许永平 03:15 -那我现在就要接。 - -卡若 03:16 -然后让我把这一些数据传到我们中台,跟把中台来完善我的数据怎么处理。这个我目前是直接抓传到中台就存客宝了。 - -许永平 03:29 -传到中台就什么宝吗? - -卡若 03:31 -这不管做,存。存客宝还是中台嘛?就是你现在弄的这个,我怎么样清洗数据过来和我的数据那个合并到那个上面去,你得有一个东西吧。是要的。 - -许永平 03:44 -是要的,那这个得要固定格式了。 - -卡若 03:44 -是不是我?这个得要固定格式吗?也不一定固定,就是我现在,像我现在要调,我怎么调嘛? - -许永平 03:48 -不一定固定,就我现在像我这边要调调。 - -卡若 03:52 -调,我是有开放,不是有开放接口吗? - -许永平 03:52 -调,我是有开放,不是有开放接口了吗? - -卡若 03:55 -那接口在哪? - -许永平 03:56 -接口在哪? - -卡若 03:56 -说明有吗? - -许永平 03:57 -有文档,这,对,我把文档发一下。 - -卡若 03:57 -对对,我把文档发你。 - -许永平 04:01 -发过去。 - -卡若 04:05 -开发完了就这条吗? - -许永平 04:05 -好,就这个吧。 - -许永平 04:15 -总道路产能群。 - -卡若 04:17 -我现在这两个是搜索的。 - -许永平 04:18 -不过现在这两个是搜索的,你这边是要存到数据中心标签里的,是吧? - -卡若 04:22 -这边是要存到数据中心标签里面的,是吧?存跟查跟完善呢。 - -许永平 04:27 -存的查。 - -卡若 04:29 -目前开放的是两个来查询的,因为。 - -许永平 04:29 -目前开放的是两个来查询的。 - -卡若 04:32 -这边上查到那个存客宝。 - -许永平 04:32 -因为这边是要查到那个乘客的。 - -卡若 04:34 -就我怎么操作? - -许永平 04:35 -就我怎么操作的地方,是吧? - -卡若 04:36 -我现在操作的地方我都不知道。就我怎么操作? - -许永平 04:40 -就我怎么操作? - -卡若 04:40 -我就我们正常存客宝那边,就是如果有就弄了两个,你给我一个接口就可以了,让我自己去调,就这一个。 - -许永平 04:42 -就我们正常使用的时候就漏了两个,给我一个接口。让我自己去调。那我得部,我还没部署,我得部署一下,我看他本地停,我部署一个吧。 - -卡若 04:51 -我还没部署呢,我得部署一下,开个本地部署一个。就大概长啥样? - -许永平 04:55 -大概咋样去拍你这些接口都是那个。 - -卡若 04:56 -去哪里拿?这些接口我都是蒙的,我自己弄的时候我接不上,知道就。 - -许永平 05:00 -那个时候接不上。 - -卡若 05:02 -比如我现在那些数据完善,包括那些。 - -许永平 05:02 -行。我现在那些完善,包括那些。 - -卡若 05:05 -数据完善。 - -许永平 05:06 -数据完善,那现在还有个问题,你是打算存什么东西? - -卡若 05:06 -是吧?现在还有个问题,你是打算存什么东西?还是说你直接想把数据库丢到拉萨呢? - -许永平 05:10 -还是说你直接想把数据库丢到拉索呢? - -卡若 05:12 -我,我不用,我就存到。 - -许永平 05:13 -不用吧。因为我现在开放的就只有查询,我开放只有查询。 - -卡若 05:14 -现在开放的就只有查询,我开放只有查询。就是查询跟接入这两个都是一回事,就以我现在这个小程序写完了,写完之后小程序这些用户我得完善到我们的那个数据中台啊。 - -许永平 05:19 -就是查数据,这两个都是一回事的,我现在这个小程序在写完之后,小程序的一些用户会完善到我们的。 - -卡若 05:33 -是不是你得给我一个地方传进来,对吧? - -许永平 05:34 -你得给我一个地方传进来,对吧? - -卡若 05:37 -第二个的话我得调,我得从中台调数据去完善我的呀。 - -许永平 05:37 -第二个的话调,从中台调数据去测流量。 - -卡若 05:42 -哦,这样,这个交互了完善。 - -许永平 05:43 -这样子,这个交互再想想。 - -卡若 05:45 -是不是? - -许永平 05:46 -就是。那这个标题,那这个数据现在的模式是这样的,我们 lark 上有很多数据库,我是从那个那里面直接把数据库就统整合。 - -卡若 05:58 -那里面直接把身份证都跑整合整。 - -许永平 06:02 -整合取到那个数据中心的,那你这边的话其实也是可以直接把数据库丢到门店大厂来,我觉得也不行。 - -卡若 06:03 -提到这个数据中心的,那你这边的话其实也是可以直接把数据库丢到我们一定要返回的一个集群。我肯定不可能丢拉萨,就是我只要调你给我数据,对吧? - -许永平 06:10 -我肯定不可能丢到大厂,就是我只要调给我数,对吧? - -卡若 06:14 -那你数据你怎么给我? - -许永平 06:14 -那你数据你怎么给? - -卡若 06:16 -那这样,我就针对你这个项目再做一遍。 - -许永平 06:16 -那这样,我就针对你这个项目再构建。 - -卡若 06:19 -不用,不要这样,任何项目就都要一遍的,就是我怎么查嘛? - -许永平 06:24 -就是我。 - -卡若 06:25 -就比如我现在用户里面,用户。 - -许永平 06:27 -查,查的话文档这里是有的,我们看一下,能查,现在我。 - -卡若 06:28 -这里是有了,我们看一下能查,现在我这。你要纠结的是你数据要传给我,我要怎么去处理这些东西? - -许永平 06:32 -我现在比较纠结的是你数据要传给我,我要怎么去存你这些东西? - -卡若 06:36 -对啊,那这个不是有一个那个 AI 的那个标签的那个东西吗? - -许永平 06:36 -对,那这个不是 AI 标签,没有看到,你知道就我们的数据都还是作用在哪去使用的这种没有看到。 - -卡若 06:39 -那个我没有看到,知道吧?我没有看到,我也不知道这个东西怎么去弄,就我们这个数据中台的作用在哪里?怎么样去使用它?重点的是这个我没有看到,知道吗? - -许永平 06:55 -我想想,我现在我得找。 - -卡若 06:56 -那我正常如果自己查询,我本机查一下就完事了,我无非把这些用户导出来在本地数据。 - -许永平 06:57 -那我正常如果自己查,我可以去查一下,会把这个用户导出来。 - -卡若 07:02 -误查一下就完事了,那我们有没有接口能查吗? - -许永平 07:05 -那我们有没有接口可以查吧? - -卡若 07:07 -接口有这个现在是可以查的。 - -许永平 07:07 -接口有这个现在是可以查的,这个的话是昨天有一个,不是有个 IPR key 的管理吗? - -卡若 07:11 -跟他操作,怎么怎么操作?怎么操作?怎么去对接,这个比较重要。昨天有一个是 IPIP 的管理吗? - -许永平 07:30 -我觉得我得把它马上部署出来,不然。对啊。 - -卡若 07:33 -这个是t,然后你拉一个t,这是 80 区。 - -许永平 07:38 -我昨天应该是有个这个吗? - -卡若 07:43 -就你们,你。 - -许永平 07:44 -这个先首先要在这边创一个key,有个 key 管理,等一下我部署,马上部署,然后部署完之后这边你创一个key,然后你等下接完文档的时候,你。 - -卡若 07:44 -这首先要在这边创一个t,我要t,等一下,我部署了,马上,然后部署完到这边你创一个t,然后你等一下接文档的时候,你把这个带到那个请求头部,你就把这个替换请求头。 - -许永平 08:02 -对吧? - -卡若 08:08 -先看一下嘛。 - -许永平 08:09 -到那个开启不错,这个地方。 - -许永平 08:48 -创建完 key 之后,就是在请求这个权限校验 key 传过来就好了,然后你就可以存。 - -卡若 08:49 -就是在请求从这边这一个一个学费校验,把这个替换了就好,然后你就可以传。这个是属于传,那我要拿数据嘞?对,再就是拉数据,然后你传的那个我没写,反正你现在提了一下午。 - -许永平 09:02 -对,这就是拉数据,然后你传的那个我没洗嘛传的,你现在提了一下午。 - -卡若 09:08 -就是我传给你。 - -许永平 09:08 -就是我传给。我。 - -卡若 09:09 -就远志那个,你要申测所那边那些完成了哪一些或者怎么样? - -许永平 09:13 -完成的联系,或者现在这一次。 - -卡若 09:15 -什么情况我现在是不知道的,知道吧? - -许永平 09:20 -我这边。 - -卡若 09:20 -我这边。反正是模糊的知道,因为我现在就提很简单,我现在对到这一步,我用户要对进来跟完善,没了我现在。 - -许永平 09:29 -那还没做完吧? - -卡若 09:29 -做完,你现在应该是只搭了底层的东西。 - -许永平 09:30 -你现在应该是只搭了底层的东西。 - -卡若 09:32 -对,我现。 - -许永平 09:32 -对。 - -卡若 09:32 -但没做完,我还没有存。 - -许永平 09:32 -我现在没做完,我还没有存这块,我不做,只是有查。 - -卡若 09:34 -没有做。那得。只是有查。有查的,怎么查? - -许永平 09:37 -有查的。 - -卡若 09:39 -查的就是接那个接口,根据传那个手机号、身份证。 - -许永平 09:39 -查的就是接这个接口,根据你传这个手机号、身份证、微信号过来,然后是一个数组的形式,这里都有传参。 - -卡若 09:43 -身份证。身份证微信号过来,然后是一个数组的形式,那给到我。那这个文档在哪里? - -许永平 09:50 -文档在哪里? - -卡若 09:51 -怎么样去动? - -许永平 09:52 -就发了,现在。 - -卡若 09:53 -不是发群里,我们在哪里能查得到,或者有一个链接,或者有一个接口的文档可以看的地方,不然我们就文档每一次都是。在弄文档的地方嘛。 - -许永平 10:03 -哦,那我部署一个bug,部署一个那个接口的地址我加一下,等一下我加一下,不然每次文档这也的确不是很友好,或者我写在。 - -卡若 10:11 -不然每次我在这确实是很不好。就你们文档管理在管在哪里?有没有一个统一的地方?比如接口。什么接口? ABCDE 的接口的文档? API doc 你有装吗?我没有,没弄。那你装 API doc,老王拉,我们平时都放在 API doc 里。 - -许永平 10:29 -放在 API force。 - -卡若 10:30 -拉,到时候我给我放出去。 - -许永平 10:31 -对。他团队里,然后。 - -卡若 10:32 -这里面,然后。没有,这个有些得先开出去,不能绑定。就是现在,嗯。公开出去得创建一个沟通方式。就这个,就东一块西一块,我的感觉知道吗?就没有。 - -许永平 10:47 -没有,就这,就我这个是没有写的,其他的老王都写了。 - -卡若 10:48 -这个是没有写的。 - -许永平 10:54 -对,你下载一个,等一下你发个链接到群里。 - -卡若 10:54 -对,你下载一个,然后我。到时候。等一下,你发我。我也发一个邀请给你吧。就你们像碎片时间这些也没有接过吗? - -许永平 11:06 -像碎片时间,结果把这些用户。 - -卡若 11:08 -这些用户的这一些,你得有接过这些东西啊。他都没开发完,存客宝都还没接呢。没有,这个是标准,你不用,不一定要数据,就是你。然后提前先创建啊。 - -许永平 11:21 -然后提前先创建了。 - -卡若 11:23 -你得有,你不是你做完了这个东西,你有东西能把那些东西字段传过来都可以了,就这一块清。 - -许永平 11:31 -想想。 - -卡若 11:32 -息,知道吧。那我们。 - -许永平 11:35 -那我们把所有的。 - -卡若 11:35 -就是我刚刚的那个需求就非常简单,这个东西弄完了,我要用户完善结束没了。 - -许永平 11:40 -好。 - -卡若 11:41 -但是用户完善这一个东西你要提取,那我没有提取的地方,对吧?都不用说我写入的事情了嘛。 - -许永平 11:46 -对对,是,是我,我没发布上去呢。 - -卡若 11:47 -对。就提取,我现在也提不了,对吧?那这个是就不清晰嘛。我没法录上去。是不是? - -许永平 11:54 -对对,那我马上发一个,然后那存呢? - -卡若 11:54 -对。那这个就是卡点呢?然后那。存肯定也要这个,就是规划的问题,就我们这个之前都讨论过。 - -许永平 12:05 -存,现在我都没想好要怎么去存你的这些东西,我最早的时候是直接把数据库丢下来,那个有哦,你是说那个且。 - -卡若 12:12 -时候是直接把数据。不是,最就是我把用户数据丢到 AI 去,我们当时还讨论了很多字段的这一些东西,标准的字段。那你进来之后 AI 聊完标。不是直接打标签就好了吗?标签引擎进去,进去完善这一些东西,那这个库。不是。当时是针对消费记录,消费记录是你那边给个,你那边把消费记录传。 - -许永平 12:33 -当时是针对消费记录,是参考抖音的产品。 - -卡若 12:39 -它标签有分几种类型。比如说一些基础信息的,嗯,标签你是直接去创的,像你说的那些什么消费的那些,它是要经过清洗的。 - -许永平 12:42 -比如说信息的抄的,像你说的那些,他是要。 - -卡若 12:51 -对。 - -许永平 12:52 -对。 - -卡若 12:52 -清洗完再那个清洗完出你要存的那个类型,比如说消费类型或者什么类型的,这些是要清洗的。但是你肯定调的就是调你整个第一就是他需不需要清洗啊? - -许永平 13:03 -直接调的就是。他是不是要进行的地址进行创建标签? - -卡若 13:07 -如果不需要他就直接写放标签嘛。嗯,对应是你的标签写进去的,那如果是说要,他要再过一层。 - -许永平 13:11 -创标签写进去啊。对,现在不是来清洗,不可能那么他自那么自动让他自动去生成标签,是。 - -卡若 13:16 -现在标签肯定是要有清洗之后的,到时候是来清洗的,可能还没那么多智,那么智能让它自动去生。对,因为里面涉及到一些。转换的东西,所以这个这一块也是要做。那现在那个读书的是打算弄哪些标签? - -许永平 13:34 -那现在那个读书的是打算弄哪些东西? - -卡若 13:38 -你是要直接把他的消息订单记录进来。 - -许永平 13:38 -你是要直接进这个,你不是要存进来。 - -卡若 13:41 -这个跟标签其实没有太大的关联,就是我们 AIAI 来负责这个标签,因为你不可能定义所有的程序的标签。 - -许永平 13:49 -如果是 AI 来负责这个,我需要你把表的结构搞定,让他去分析。 - -卡若 13:50 -看,负责这个,我需要他表的结构绑定的,让他去看。表的结构他自己会去分析。 - -许永平 13:55 -表的结构,他自己简单地说一下,就这个用户管理这一些东西。 - -卡若 13:56 -我,我简单地说一下,就这个用户管理的这一些东西,比如他看的这。 - -许永平 14:02 -看看这个用户卡的底下有个什么样的,对吧? - -卡若 14:02 -这用户看了什么?底下有绑定什么样的人,对吧?什么样的时间,对吧? - -许永平 14:06 -什么样的时间,对吧? - -卡若 14:08 -阅读了什么,发布了什么,他那些行为轨迹都得传到相应的标签的体系里面来,是不是? - -许永平 14:16 -就是你要搭一套清理的规则,它是一个动态的。 - -卡若 14:16 -就是你要。你得范式。一套清理的,清洗的这个规则,它是一个动态的。这个肯定。 - -许永平 14:24 -但这个也是要根据需求,什么数据进来,你说一个大仓库,那就丢什么嘛? - -卡若 14:25 -不管是丢什么数据进来,它是任意丢的。你说一个大仓库,然后他想丢什么就丢什么。然后他会。 - -许永平 14:32 -然后。 - -卡若 14:32 -通过那个清洗去给他判断哪些是可以提炼成标签。那种的话最好,那肯定是要整个可能会丢,每张表的那个字段不一样,我没办法说创建一张那种全部都是空白字段,空白那个限制的表。 - -许永平 14:37 -那种的话最好,那肯定是要整个布局,你那张表那个字段不一样,我没办法说创建一张那种全部都是空白字段的,那个禁止的表。 - -卡若 14:50 -不用空白,就是他进去之后按你的标准去留存就行了。就比如说他这边有个比较他正常的注册这些信息,这些还有的就是他的消费记录,大概就是这些要一些。 - -许永平 14:54 -就比如说他这边有的就提交他正常的注册这些信息吧。 - -卡若 15:02 -就跟它应用场景有关了嘛。就是我们给一个规则,让它把那些事物关联起来。因为。目前目我们当时做的就是先给,先把那个消费记录先跑过来,所以很多库我都直接把有消费名单的进行转化,这个是有做的,那现在就是新在消费记录上你还要生成什么样的数据? - -许永平 15:12 -目前目我们当时做的就是把那个消费订单的这个情况是有做的,所以现在新的消费记录你还要新增什么样的数据表?这个我得新增。 - -卡若 15:31 -这个咱们上次。 - -许永平 15:32 -我们上次讨论的这一个这个流程的话,我觉得你再去确定一下,现在的话如果这个是有卡点,我们开发下去才知道会有。 - -卡若 15:32 -讨论的这一个流程的话,我觉得远志你再去确定一下这一个东西,因为现在的话如果这个是有卡点,我们开发下去才知道会有卡点,那怎么去把它迭代掉呢?是不是?这个。 - -许永平 15:50 -对。 - -卡若 15:50 -因为这个需求本身,如果我在做这个事情都有卡点的情况,我觉得任何人都会有卡点,因为我是非常灵活了已经,知道吗?我。我现在就是这个东西,你就给我一个API,就我都全部搞定了,是不是?那我现在都会有这个卡点都我自己走一遍,我就很清楚了,就我们这数据中台的整个的这个搭建,对吧?你还是按规划来吧,不然这个的确是不知道。规划之前是按照,得提前先设定好标签,按照之前有调研那些的确是有。 - -许永平 16:21 -规划之前是按照,得提前先设定好标签,按照之前有列的那些,的确是有。 - -卡若 16:27 -标签基本库是用来放的,你就建好一个个箱子,对吧?那。你中间有一个分类源,那个 AI 的标签引擎就是这个分类源嘛。那我传的那些标签过去,它分类成完之后就丢到你这个一个个箱子里面就行了嘛。它逻辑本身就本质上就这个事情,你定义的就是。 - -许永平 16:46 -定义的就是定义的一些大类,他们一起玩这个是属于什么类的? - -卡若 16:47 -定义的一些大类啊。就这一个。这个是属于什么类的? - -许永平 16:52 -哦,我想想怎么整?那行,老,下一个,老王,先继续,我想一下这个怎么整吧? - -卡若 16:59 -这个你一定是跟那个神射手去结合的,不然一。块一块感觉会没有总整体性,你知道吧?就我不知道他的到哪一个地方,知道吗? - -许永平 17:06 -好,行,先这么说。 - -卡若 17:11 -然后我,我会,我这个,我先说完这。行行行。我会有一点是什么?就是我现在为什么今天没有让你去把这个输的这个?因为有很多的一些细节的东西,我就想试自己试一下,走,跟你这个对接一下,看才能发现问题嘛。不然就肯定会那个的嘛。会有更多的一些卡点,或者不能往下去走,对吧? - -卡若 17:51 -现在就,所以这个还是要按板块来,我们好像一直都在原地踏步循环开发,然后回那个。 - -许永平 17:53 -对。 - -卡若 18:04 -回溯一些东西,知道吧。 - -许永平 18:06 -另外一个,你是。 - -卡若 18:06 -一百个。没有一个地图展开的感觉,知道吧。就抽象点说的话,是这个嘛?那这个就规划跟原来的那个东西怎么样去融合的一些问题嘛?所以你们在那个项目管理上面的话,我觉得还是要明确清晰吧。因为没有图嘛啊?往下吧,往下这个,这个你看一下,看怎么解决嘛?因为我现在这个我肯定是要上线的,本来周一要上,上,周一要上线,上周一就卡。验证嘛?那这个时候验证过了,就那个,这有一些那些 bug 我都还没有去弄,本来这个东西,这一个用户我早上就在过来的时候,我就才弄这个用户了。那用户弄完之后,我说下午那 API 过来,我就接上去,那咱们就正常可以使用了嘛。 - -许永平 18:57 -做完之后有效。这边这个计划。 - -卡若 19:02 -对吧?那现在又就这边就卡住了,是不是怕你也一定会卡住的?就这一个,到时候要写上就解决这个问题,然后要有个清晰的文档吧。然后关于那还有个提现的,今天是不是知道说这个虚假新开的说云审查的你因为提现,我现在后来看的是云审查的模式。然后上面这个名字,我们点击他这个,再跳出来就是他的主页。能提确定收款吗?已经到了吗?得点这一步,再点确认收款才可以。 - -许永平 19:43 -再一步再点确认购买。这不是地点物流。 - -卡若 19:46 -是避免不了的。这样子就会到账。好。 - -许永平 19:51 -你好,稍等。 - -卡若 19:51 -这样就到账了。可以,嗯,永平这个也不着急,就是要一点点去。 - -许永平 19:57 -只有一刷新。就这两个,就这两个状态。 - -卡若 20:02 -我们反正一个事情过吧。 - -许永平 20:03 -对的。然后。 - -卡若 20:05 -然后都把信息给他,嗯,提示。然后你。那这个不是能刷单就很爽吗?对。自己刷自己钱。 - -许永平 20:14 -然后你。 - -卡若 20:22 -昨天是没刷。 - -许永平 20:22 -昨天是没传。 - -卡若 20:23 -这么丑。 AI 我都是写的四个字,生成事件之间的,来到没了,没给他描述。这个也太。那我只是为了让你尝试一下。行吧。之后有一个卡点,我证书一直申请覆盖不了,就你有两三个证书,这几个月要,这几天要过期了。那你就是再申请。让你登录发验证码。我不是发了吗?我知道,一直卡,记住我,他提示是已经可以了,但是我必。第二天去看他的灰度高,灰度把进入0%。这个我一直卡着,我一直在看这个到底是什么问题导致的?我到时候让,到时候我再去研究一下支付宝那个叫。对,这个,那个他们那个钱的充值,你要怎么走那个流程啊?谁钱的充值啊?就是你总账号,他肯定要有钱嘛。那比如说那个瘦那边,他们是他们去审核,你跟他合作这个钱不应该是我们垫付,是应该他们要先把钱。项目打啊。对,那你要怎么走?怎么。这个就是项目为准的。直接他。项目充值到我们后台,然后直接扣啊。那你还要再做一个充值的。这一个后续再说。 - -许永平 21:54 -这一个后期再说。 - -卡若 21:55 -我知道,就是你后续你要直接做一个充值。你这个充值也没用。那你相当于你要开个体验版。走线下。不不不,你们,你这线上充值没有人会线下的。你走线下有好处,第一,你走线上的话,我不知道你这一个手续费是多少,你先。 - -许永平 22:13 -走线下。对。 - -卡若 22:18 -那你不用去思考这个问题。我知道。这个,这些就是利润的问题了,你。 - -许永平 22:23 -对。 - -卡若 22:23 -利润没错,白也没错,你充进去他还是扣不了,你得再手动再充进去,他现在拆下来。 - -许永平 22:24 -又没错。他还是通过他现在才。 - -卡若 22:31 -我知道本地。用户跟运营户嘛?这个写个脚本就行了,我都实现了,这个你不用去思考这个问题,能实现他能,就是他到时候短信你按一下,他会你签转账的时候就是授权一下就结束了。你这都有脚本了,这个没有啥,你不用担心这个问题。 - -许永平 22:48 -如果你在一个学历1万,我们。 - -卡若 22:49 -我这个可以做。你反正你去看一下嘛?这个不是一个难,就本地户,这个到时候要转到运营户嘛。肯定做。提本户转运营户嘛。一个对应的后台。他就是给。商家后台。 - -许永平 23:02 -算一个。 - -卡若 23:02 -我会做到新版的那个碎片时间。 - -许永平 23:04 -对。 - -卡若 23:05 -对呀。 - -许永平 23:05 -对。 - -卡若 23:05 -不会再拉出来了,这个只是说让他临时可以用。现在就是先线下,那个线下打,如果他们要用的话就先转一笔进去嘛。现在就我们自己打嘛就行了,那你得规划出来。目前先走线下,他转给我们,然后我们打到那一个运营账户,然后再从运营账户扣钱。是这样的流程,然后去做。 - -许永平 23:32 -给我 c 的。 - -卡若 23:32 -我新版的那一个碎片时间之后再。再说你说的那个流程嘛。不然这样子碎片时间也不用太多时间。然后我那一个标签最近今天注册掉了10%,在对接接口。然后业务也在搞。之后,明天就可以全力搞那一个流量池碎片时间去搞一搞,多弄。刚刚提交审核的时候被驳回,让他再重新提交一下。驳回原因呢。驳回。 - -卡若 24:00 -応援今の。 - -许永平 24:00 -会。 - -卡若 24:03 -他那一个得看审核人员的信息,同一个页面,同一个东西,他驳回的理由都是不一样的,有时候会通过,有时通过不了。那你就多提交几次嘛?对,所以说我待会都会重新再提交一下。那可以,那你就继续往下嘛。那我明天就继续全力突击标签,把,标签大概这层可以让你有一个城市的方案。就那个嘛?就正常这种还有吗?没有。那你。那你那个碎片时间要跟永平那边对一下,永平。永平,你把你那个。 - -许永平 24:36 -好好好。 - -卡若 24:38 -就是等一下你把那个标签的那个用那个展开,我不是有那个文档嘛?你展开就到时候对一下。对吧?有一个架构什么你才比较清楚嘛?展开,你把这个那些那个源码和数据库丢进去,自己展开,让它变成说明文档,我们看一下流程嘛。那你就也要弄一下,看看一下,对齐一下,反正比较清楚嘛。这个主要给远志看的,远志看了没问题,那就没问题。说明文档概要把那些比较重要的。就你们做完自己生成一个,你们检查你觉得没有问题。 - -许永平 25:13 -对。 - -卡若 25:14 -就跟你那个昨天那个实际的分析,大概实际的差不多,他分析出来了,然后还拆解了,还提一些各种的案知道,还跟一些说提这个项目以后的规划流程了,干嘛干嘛。对,就之类的,反正你。这个文档我是已经思考了大概 18 个点。就那个,你们是要对什么?那个,就那个。 - -许永平 25:40 -就所以现在。 - -卡若 25:42 -对,现在我卡了一条碎片时间聊天系统,例如我做了什么海报的任务,我就把一些信息就给永平,然后永平收入信息,收入显示之后再一个事件是在这边,然后他做了什么任务之类的,你要跟他的用户画像之类。打标签嘛。对。打标签,但是现在的问题好像是打标签这块你还没弄完。是吧?他这一块就是只是支持查询。 - -许永平 26:09 -现在我觉得是更新其实是有的,就是现在这一套是基于上面不是已经有数据库了吗? - -卡若 26:13 -对呀,要说支持更新,对呀,没有写更对,更没有写。但是要更新,更新其实是有的,就是我这现在这个就基于它上面不是已经有数据库了吗? - -许永平 26:26 -应该都是全部数据库,只要那个数据库有更新,我的数据有更新。 - -卡若 26:26 -那都是全部抓在你的数据库,只要数据库有更新,然后数据中心它就会自动更新的,因为它任务都是。 - -许永平 26:32 -多都是在跑,然后像做担心,最初在想是直接把数据丢进来就好了,因为他做那个淘宝那的话,我们那个接口跟本地的这个字段肯定是有不一致的,所以我的想采访 AI 带的话,就省去了换这个过程。 - -卡若 26:33 -然后像你们说的那个更新,我最初在想的是你直接把数据库丢进来就好了,因为它会自动去抓,做那个 AI 也是这样去抓的,这样的话我们那个接口跟本地的这个字段肯定是有不一致的,所以我的想,我初衷的想法是有这个 AI 在的话,就省去了对字段这个过程。那我知道。是啊,就 AI 对字段的,但你不要用本地,因为模型没有用。 - -许永平 26:53 -是 AI 啊。 - -卡若 26:57 -我明白意思,因为它 lark 后台有一个地方添加数据库,它直接把数据库账号。密码,然后名正他们输入进来,让 AI 这种分析嘛。 - -许永平 27:06 -对。 - -卡若 27:06 -我知道最早解决方案是这个,所以你这个标签的 AIAI 的分析的引擎一点有个逻辑在那边,现在是没有看到,这个很重要。对。 - -许永平 27:17 -是。 - -卡若 27:18 -知道吧。 - -许永平 27:18 -搜索上面都对的。 - -卡若 27:18 -上面都有。就是永平现在做的那一个,你要知道你做这个就是纯搜索的一部分在哪一部分上面? - -许永平 27:20 -只有永平现在做的。 - -卡若 27:26 -然后就远志规划的那一部分是不完成,然后我们看就验证那个嘛?解开发小程序什么,这些都是在验证,知道它并不是一个什么,那个什么。到时候那个小那个神社所那版先发给你,发他,对的,那个,看那个。 - -许永平 27:42 -就是厕所那块先发给。好,发我看。 - -卡若 27:47 -我们的核心的东西是啥?不管小程序这些,这太容易开发了,你搞进去用户数据跟我们做交互合并和估值,这是我们的核心,知道吧? - -许永平 27:54 -对。 - -王名正 27:54 -用户数据跟我们做交互。 - -卡若 28:01 -不是小程序,小程。小程序没有任何价值知道吧。所以这个这一块是非常重要的一些点,包括一些小的一些 bug 或者什么的调整嘛。是吧?可能你们修 bug 可能会快一点,老王修 bug 跟永平修 bug 的这一些,但我们不要生产太多bug,修是很快,对吧?不然东东一块西一块漏是不太好的,所以我才。诶,一直说咱们有一些完整性一点的东西,是吧? - -许永平 28:38 -我这边的话有个数据采集,其实在这边就是来配数据库了,如果是说行。 - -卡若 28:38 -我这边的话有个数据采集。你永平,你看一下这个怎么融到厕所里面去? - -王名正 28:43 -没有,你有投屏的,我看得到。是许永平没有放上。 - -卡若 28:48 -我觉得这个是不是这你看一下。 - -王名正 28:51 -远志投屏的,你试过去一下,远志投屏的,远志的。 - -卡若 29:02 -那。个今天跑,站在一台式上面跑。跑,你这个切片的。是第一个碰到的问题是。你现在很多是第二,你封装的完是你的苹果环境。 - -王名正 29:12 -我们都得二次转换,我们,我们得先改写 Windows 版本的。 - -卡若 29:15 -那他自己会去装,我知道啊。对,然后我们现在就是拿到这些的话,有时候一开始跑,他会那个他环境不一样,他变成得改写,改成对的版本。对啊。对,他 AI 会自动改写,然后可能要有一些环境要重新装什么依赖的。对啊。然后装完。的话,跑完的话我这次跑,他第一次跑的结果,因为我不清楚嘛。他跑完的,结果跑了好几个小时。那不是很正常吗?但是。说你跑一个视频多少?几分钟就好。 3 分钟就搞定了,你好几个小时,是你没有指定 MLX 的那个 Viser 这个。不是。加 MLX 杠,不然它就变成很麻烦的。你听我说,就是一开始我不清,因为我不知道,然后他跑了几个小时,我觉得不对,然后他。整个CPU。干,干,翻了。那不很正常嘛?你以后像这种你加一句就是。好,我跟你说像这种多任务的。最终跑了一个多小时, 97 死机了。你先听我讲完你这个,你就这要限制一个,大家就是在做运行的时候限制在 CPU 使用百分之,整个机器性能70%,不要超过。 - -许永平 30:13 -你先听我讲完。 - -王名正 30:24 -掉。 - -卡若 30:24 -不对。你听我讲完。他问题是他没有调那个显卡,他要用显卡去处理,不应该用 CPU 去处。 - -许永平 30:26 -他问题是。 - -卡若 30:32 -那就。就掉显卡嘛。对,所以说这个就是属于逻辑上变清楚嘛。 - -许永平 30:36 -他说我现在后面几个小时就死机啊。 - -卡若 30:36 -说我现在后面改完的第一次跑 CPU 就变得不行了。那跑g, GPU 就快啊。一个几个小时就死机啊。那跑成了没有嘛?然后现在是我改成显卡,显卡现在是跑成了,我看一下。那不就对了吗? - -许永平 30:52 -你这里说 10 分钟。 - -王名正 30:58 -你点击应该可以跳出来。 - -王名正 31:11 -你复制,你再复制那个链接。 - -王名正 31:30 -桌面。 XYZ 视频凹凸输出。 - -王名正 31:44 -1 起 1 + 5。 - -许永平 31:49 -没视频文件吗? - -卡若 31:56 -啥东西嘛? - -许永平 31:57 -接吻。 - -卡若 31:57 -你是刚切完片。 - -许永平 31:57 -你就发希望,嗯。 - -王名正 31:59 -行。 - -卡若 31:59 -你这一个是,估计是他的那一个话术吧。 - -王名正 31:59 -你这一个是,估计是他的那一个话术吧。 - -许永平 32:00 -再一个是我。 - -王名正 32:02 -话术啊。什么话术? - -卡若 32:07 -这是文件,你视频没跑出来。 - -王名正 32:07 -这字幕,这是字幕。 - -卡若 32:10 -他说已经跑完了,就是没,但是没看到视频。那你视频放,你看那个 cursor 上面嘛。对,就是看这个东西。视频有文档吗?输出日志。输出啥字?等我看一下,字这么小。哦,你还要自动,那你怕你还要自动再运行一下按钮的这个日志。 - -王名正 32:25 -哦,你还要自动,那你他,你还要自动再运行一下 Python 的这个命令。 - -许永平 32:28 -不要自动运行。 - -卡若 32:30 -不用自动运行,你。 - -王名正 32:30 -没有,他以后。 - -卡若 32:31 -没有它以后。 - -王名正 32:32 -切片说运行这一个。 - -卡若 32:36 -就说以后,但是现在这个转入完成。 - -许永平 32:36 -说以后,但是现在这个转入成,我看一下。 - -卡若 32:42 -我看一下,回去。 - -王名正 32:45 -他啥? - -许永平 32:45 -太强了。 - -王名正 32:45 -我说用手机再放大看他们,他只是说一个文件没有。 - -卡若 32:47 -就那个吗? - -许永平 32:48 -这个吗? - -卡若 32:48 -输出设置这个是文档剪辑、视频剪辑项目 output 这个,然后。 - -许永平 32:49 -输出设置这个是文档吗? - -王名正 33:02 -我没有视频,没有 case 的视频,没剪视频。 - -卡若 33:02 -没有视频。没剪视频,但是剪视频很快,你它这个是你已经拆解完了,就是没剪。 - -许永平 33:10 -然后我又加了几个需求,就是。 - -卡若 33:10 -然后我又加了几个需求,就是加了几个那个因为经常跑这种大的时候,它就一直卡在那边,就我不知道它是不是在跑还是卡住。那你就告诉他,实时给你反馈进度。 - -许永平 33:21 -那你就告诉他说,我们这个才知道最终的。 - -卡若 33:23 -加了一个。不就行了吗?对。但这个得完整跑一下,到最终的流程是怎样?那肯定,主要是。那台机子它能跑就可以了,我们这个你只要能跑,后面就是文件,还有它有一个自动上传到抖音或者什么什么群。 - -王名正 33:36 -这个就是文件。 - -卡若 33:41 -那个是分发嘛。分发的嘛。现在就是先跑剪的嘛。你先跑,剪这种事情。这种你到时候如果要给他们用的,是得录到那种比较好的那种。那你搞云机就好了,搞 GPU 就好了。你这种机制就没办法。或者搞苹果是最快的。苹果对,你那个没有。对,这很慢。那你就。 - -王名正 34:04 -你听过最近的是挺快的,但是哪一点的例子不好说? - -卡若 34:04 -苹果可能自己快,那老一点是一种不好。老一点也快,苹果的显卡就是不一样。就不一样,我跑一下 50 秒,拆解是三提取三到 5 分钟,跑完剪就 50 秒。 - -王名正 34:12 -没感觉,我真的没啥感觉,我那台跑起来也挺卡卡的。 - -卡若 34:22 -嗯,那你没有用那个嘛?没有用那。 - -许永平 34:24 -我还是苹果,它那个系统优化的比较好。 - -王名正 34:26 -主要是我那台太老了,那台苹果好像是19。 - -卡若 34:27 -主要是我那个太拉了,那还有一苹果好像是19。 - -王名正 34:32 -连麦的。 - -卡若 34:34 -19 年,对。然后这几个我得完整跑完,我才知道问题在哪里。你先跑一遍嘛。然后数据员工的话,我这边是改成这个,之前是那个卡片折叠的,我给他改成这种,就播报数据的话,还是卡片下面一个,就是以这种,不是跟他。一样的卡片这种推送,然后估计的话就是,嗯。 - -王名正 35:12 -你测试用户是哪一个?还记得? - -卡若 35:16 -如果我输的这个是冲突的,它里客户库里面有的话,它就会导出它的故事,那如果是说如果是查不到的,它。 - -许永平 35:21 -客户库里面的一个。 - -王名正 35:29 -陈佳。 - -卡若 35:32 -客户资料里面把这个跑到冰娇那边来,添加好友发送需求。 - -卡若 35:49 -这个老王开始弄了没有?就是碎片时间弄完就搞这个嘛? - -王名正 35:52 -家垒不是在弄那一个标签吗? - -卡若 35:54 -不是在弄那一个标签,先,标签完之后再做一个。 - -许永平 35:54 -就在标签吗? - -王名正 35:55 -新标签,标签完之后再这一块都有排期啊。 - -许永平 35:57 -就在。 - -卡若 35:59 -都有排期啊。 - -卡若 36:10 -剪辑,是吧? - -许永平 36:10 -点击。 - -卡若 36:11 -就是处理那个问题。 - -王名正 36:11 -剪辑设置。 - -卡若 36:15 -你先一台先搞定再说,因为这个检测环境不一样,调整一下。 - -许永平 36:23 -比如说你现在。 - -卡若 36:23 -比如说你现在很多不同的steer,它里面用到的一些插件,比如说它有环境不一样,怎么去解?解决这个问题是不是还得写一个这种?不,不用,你直接问,他自己会去改啊。我知道,就是他应该还要有一个逻辑是去处理这个版本的,就比如说你调用的第一个,他可能需要的 Python 版本是最高只能是到10,但是你比如说你调用第二个的时候。 - -许永平 36:38 -都是。 - -王名正 36:38 -他应该还要有。要。 - -卡若 36:51 -可能。他会装虚拟环境啊。但是你在是一个同一个工作区吗?他在也会每一个也会用他自己的虚拟环境啊。不知道可以,你就是提示词,你加一个部署到虚拟环境里面。 - -王名正 37:03 -Windows 不行,像我家里那台电脑就是 CNP 那些命令在课程完全执行不了。 - -卡若 37:15 -这个它有虚拟环境的,你就你们。 - -王名正 37:19 -对,我在公司这一台我就可以完美执行。 - -卡若 37:23 -这个就是你一个 steer 用的就是一个虚拟环境,你多加一句话就行了,这个一定会碰到的问题,你加一个提示词,加一对,加1。 - -许永平 37:28 -把,这对。 - -卡若 37:32 -那就行了,我现在解决非常简洁,知道我就让它装到什么? - -许永平 37:34 -はい。 - -王名正 37:36 -大家装到这个微信。 - -卡若 37:38 -装到那个虚拟机里面。doc,所有东西都装doc,咱就调用 doc 就完了,就 doc 就是一个功能。 - -王名正 37:48 -你电脑好一点,性能好一点,可以这样搞,我知道它是一个容器。 - -卡若 37:51 -doc 不会占太多资源,你好好去研究一下,它不是虚拟机。对,它不会占太多资源的,它不运行根本就没占资源,跟文件夹似的。 - -许永平 38:02 -每个词。 - -卡若 38:02 -要都单独的一个。 - -王名正 38:04 -小荣幸。 - -许永平 38:04 -小游戏。 - -卡若 38:05 -对啊。布个环境。对,他可以自己去调,他没有就去装一个,自己会去调,知道吗?这个回头你就先剪跟发,能发到群里面最好也。 - -王名正 38:19 -你现在就是搞doc,每一个 schema 就一个doc,我说,我是说你现在。 - -卡若 38:20 -你现在就是搞doc,每一个 DA 就一个doc。 - -许永平 38:20 -现在就考后端。你可以搞虚拟货币。 - -卡若 38:24 -你可以搞虚拟环境,可以搞 doc 很多解决方案。虚拟环境跟 doc 混着用, doc 就。 - -王名正 38:30 -我是没有,我电脑承受不起。 - -许永平 38:30 -我是没有,对吧? - -卡若 38:33 -文件大一点,没啥区别的。现在我主要是给电脑上冲突了。 - -王名正 38:35 -现在我主要是本地电脑那台环境太多了,已经冲突了。 - -卡若 38:39 -那你一定会冲突,你要你多加一个,让它生成独有的一个环境就可以了。 - -王名正 38:46 -现在我本地环境都冲突了,再装的话也装不起来了,再装。 - -卡若 38:47 -现在我本地环境都冲突了,再说的话也装不起来。不是,你装虚拟环境就好了,哪里会装不起来? - -王名正 38:56 -名正你麦扬声器关了。 - -许永平 38:56 -我这里把声音关了。 - -卡若 38:58 -你知道我在看远程,是通知不了的。 - -许永平 39:00 -不去,远程是听不了的。 - -卡若 39:02 -是。 - -许永平 39:03 -好。 - -卡若 39:08 -有,那你这个剪出来的切片,它字幕也没有自动配,它会自动配吗? - -王名正 39:08 -那你这么点出来的推荐它自动对,它会。 - -许永平 39:08 -那你这个剪出来的切片,它字幕也没有字幕,对啊。 - -卡若 39:12 -会自动配,你没自动配,就是没加提示词。 - -王名正 39:13 -没加,你没加提示词。 - -卡若 39:15 -不是你,你不应该你原来的那个就已经是有的吗?是有啊。原来的。是有,你要变成剪那个加字幕,它有三个版本,一个不加字幕,一个加字幕的时间会多一些时间就多搞一遍嘛。时间翻倍,没其他的呀。是存在其他的。你就看看那个test,那个高速路。 - -王名正 39:35 -你就刚刚那一个test,那些都是字幕。 - -许永平 39:36 -就刚刚那一个case,对, 5 个高就切到一些空白。 - -卡若 39:39 -知道,他现在剪出来就剪了五个高光时刻出来,应该是按你的提示是剪出来,但好像有问题,一直这样闪。 - -王名正 39:54 -他剪了就会就闪一闪,闪一闪就切到一些空白的、没用的那一些就直接闪过去了。 - -卡若 39:57 -就其他一些空白的、没用的,拉一些就再返过去。 - -王名正 40:03 -像现在它就跳过生成的事件了。 - -卡若 40:06 -noor 这些他都不会。然后你上面生成。 - -王名正 40:10 -然后你中间生成等待了,它也是在跳过。 - -许永平 40:10 -然后你找那 3 个。 - -卡若 40:12 -你先剪几个做调整就知道了,这个都是你调一下就清楚了。 - -许永平 40:12 -你先写几个。 - -卡若 40:21 -是吧?现在这边跑出来了。你能跑动了就对了。调调他的那个CTR。 - -许永平 40:25 -调调他的那个。 - -王名正 40:26 -gpu。 - -卡若 40:27 -你要调。干不动,干到百分之都满了。你调模。 - -许永平 40:32 -我是直接。 - -卡若 40:32 -直接死机的,直接。你以后像这种你要跑很久的,你就限制它性能 70 80,它自己会给你限制的,你就不会出现卡和点不动的问题了。限制你变成时间还是得。那就多搭百长,百分之 10 20 又没有啥。 cursor 那边经常会断掉。 cursor 会。不了,那么长啊。 - -王名正 40:54 -这里是要至于 create 更。 - -卡若 40:55 -不会,这些都有解决方案,你们跑就知道了,它。 cursor 那边断掉你任务。它会后端去监控文档,它。一样在跑,不用在 cursor 实时给你看,知道吗? cursor 断就断,无所谓,他后端执行下去他不会,他都文档都写好了,让你按他的文档节奏去执行了,知道吧? - -许永平 41:08 -没办法。 - -王名正 41:13 -行,这个得尝试。 - -卡若 41:17 -你们走一遍就知道了。 - -卡若 41:26 -那我过一下这个永平,明天那个你小程序,明天。 - -王名正 41:30 -那个。 - -卡若 41:32 -可以给你改,已经弄完了,现在能正常付款了。 - -王名正 41:33 -因为我们现在已经,那就了解一下,嗯,对吧? - -卡若 41:35 -那就了解一下,用跑一下才知道这个问题在哪里,你自己跑才知道,对吧? - -许永平 41:38 -下回跑一下才知道是。 - -王名正 41:40 -跑一下才知道问题在哪里,自己跑,现在直接做一下,我实证验证一下。 - -许永平 41:42 -是。一跑才知道。好。 - -卡若 41:44 -因为我现在我给你过一下,我只是这几天我要验证一下这个到底卡点在哪里,所以会去那个嘛。 - -王名正 41:51 -对对,去那个。 - -卡若 41:54 -那尽可能,我们要按那个远志那边规划的时间去走嘛。 - -王名正 41:55 -那尽可能的,我们要按那个远志那边规划的时间去。 - -卡若 42:00 -我看一下这个。所以你这个阶段性的东西得给我具体一点发一下这个东西嘛? - -王名正 42:03 -所以你这个阶段性的也会具体一些。 - -许永平 42:07 -好。 - -王名正 42:07 -看一下,就这个,这一个现在的话基本就是这样。 - -卡若 42:08 -就这个,这一个现在的话它基本就是这样,我已经部署上去了,然后,诶,打不拉不过去,啥情况? - -王名正 42:24 -应该是你那个信息。 - -卡若 42:26 -就这一个嘛? - -王名正 42:27 -这一个时间的话,基本就是正常的点。 - -许永平 42:27 -一个这边啊。 - -卡若 42:27 -这边的话它基本就是都可以,正常都可以用,就可能按钮。有点歪啊。但它这些正常的功能都是正常去使用了这些匹配的乱七八糟的其他后台应该是没数据。 - -许永平 42:37 -有啥游戏? - -王名正 42:38 -行。 - -卡若 42:44 -那。前后端都好了。 - -许永平 42:45 -前后端。 - -卡若 42:46 -都好了。数据库也去接,是吧?数据库也接完了。现在是差。差,看看有没有一些 bug 或一些,比如有几个我还没空去改的,比如这个后台这个设置免费的,这个设置的参数我。你们可以记一下。 - -王名正 43:02 -你可以记一下。 - -卡若 43:03 -他们直接布上去测一下。 - -许永平 43:04 -直接布上去测一下。 - -卡若 43:05 -不,已经布上去了,都布上去了,我只是在这边跟你们说一下这个事情嘛,我说能看得到的一些 bug 嘛。对吧?有个问题。 - -王名正 43:18 -问题。 - -许永平 43:19 -没问题,我今天把你那个代码拉下来,然后看,然后他那个叫你,你是用什么东西让他自动编译成小程序啊? - -卡若 43:19 -我今天把你那个代码拉下来了。你说嗯。然后他那个叫什么?你,你是用什么东西让他自动一层一层去找的?我在云马腾讯开放文档,没找到。 - -许永平 43:30 -我在源码里面开发文档,没?找到,好。 - -卡若 43:32 -你这个我等一下说,有问题,等一下说,我先过一下这个事情,有可能这个付款都解决了,在小程序它就跳出小程序付款的,我再去小程序过一下,比较清楚。 - -王名正 43:35 -先过一下这个事情。 - -许永平 43:50 -你刚付款,不是一个环境吗? - -卡若 43:51 -不用,你在我小程序运行不是很难,我给你们过一下,这个已经是可用状态,但是有一些细节,为什么? - -许永平 43:52 -它调试工具是这样,小程序运行不会这样。我直接调用的。对。 - -卡若 44:03 -今,其实现在上线也可以,但我不想这样。 - -许永平 44:04 -现在上线可以,咱们不想这样。 - -卡若 44:06 -知道吗? - -许永平 44:06 -知道。 - -卡若 44:06 -肯定得测啊。 - -许永平 44:07 -肯定得测啊。 - -卡若 44:08 -测也七七八八了,就你像这个他就正常支付就可以了,这个数是调的数据库,就 Mysql 里面的那个数据。 - -许永平 44:09 -测了也七八八了,像这个他就正常支付就可以,这个是调的本身。 - -王名正 44:09 -七七八八的,他是正常。 - -许永平 44:19 -你现在这个好像是已经,其他今天有看那个后台。 - -卡若 44:19 -好像是已经携带飞书了,是吧?我今天有看那个后台。都,这个都自动了,这些都正常可以用了。 - -王名正 44:24 -对对对。 - -卡若 44:28 -付完款之后是能立即知道,比如。 - -许永平 44:28 -付完款之后是立即知道了。 - -王名正 44:29 -付完款之后是我们立即自办。 - -卡若 44:32 -如果我把这个东西给这一小节,这一小节我传给远志吧。 - -许永平 44:32 -过把这个,嗯,等给他之后还需要更新好自己,嗯,后台是。 - -卡若 44:40 -我举例或者传给他,我转给他之后,他只要付款,看过付款我这里就立即就知道付款了。就是我的后台是立即知道,并且能老王那边的那个自动分账的,这个我还没接,但是已经有弄好了,他这里的话我是能看到。 - -许永平 44:55 -然后那个自动就够了,自动看到。 - -王名正 44:57 -跟大诶,但是已经有动作了,他看情况能看到。 - -卡若 45:04 -谁买了? - -许永平 45:04 -谁买的? - -卡若 45:05 -我这边已付款,就一个。 - -王名正 45:06 -一个,嗯。 - -卡若 45:09 -这里谁付款的都很清楚的,然后他没付款就是绑定状态,就微信用户就他嘛,然后在后台的话,这里的话是会有一个都是用户绑定的,今天为什么对这个,这个是用户绑定? - -许永平 45:09 -这里谁付款的都清楚,然后他没付款就是绑定。 - -王名正 45:11 -然后上面是绑定状态,就是微信用户,他跟用户绑定,因为为什么会这个? - -许永平 45:16 -然后在后台的话,这里这个,这是用户绑定的,今天为什么推这个呢? - -王名正 45:24 -这一个是用户绑定。 - -许永平 45:24 -这一个是用户绑定,这个是我的微信推的这一个人吧。 - -卡若 45:25 -比如这个是我的微信推的这一个人嘛。有人进来,我绑定了谁,就绑定这个人,他付款了。 - -王名正 45:28 -有联系平台的绑定谁? - -许永平 45:28 -有人进来,我绑定了谁,反正这个人他付款。 - -卡若 45:32 -我的收益是这个是清晰的知道,而且是现在还没实现立即到账的,是吧? - -王名正 45:32 -周一去这个。 - -许永平 45:32 -我的收益是这个,那是清晰的,而且是现在还没实现已到账,我这个是清晰的,然后这边的话。 - -王名正 45:38 -好的。 - -卡若 45:39 -这个是清晰的,是吧?然后这边的话,诶,不是这个域名,等一下啊? - -王名正 45:41 -然后这边的话。 - -卡若 45:50 -就这里嘛,这里的话这边就正常的能看得到。 - -许永平 45:52 -就这里嘛,这里的话这边就正常的看得到,那我现在的实际点是很多用户中心点击进去是要看到他全部的生命。 - -王名正 45:52 -这里的话这边就正常的看不到,但是我现在的理解是我们用户中心点击进去是要看到他。 - -卡若 45:56 -那我现在的是一点是什么?这个用户中心点击进去是要看到他全部的生命轨迹。 - -许永平 46:02 -给自己跟这一个的,那这一个页面又没有接口,所以这个会出错,知道吗? - -卡若 46:02 -也跟这一个的,那这一个页面因为没有接口,所以这个会出错,知道吗?我今天尝试着去接一下你那个后台,但是没有接口肯定会出错的。 - -王名正 46:08 -我今天尝试着去接一下你那个作品,但是接口肯定会出错的。 - -许永平 46:08 -我今天尝试地去接一下你那个后台,但是没有接口肯定会出错的。没有,现在没有这个功。 - -卡若 46:13 -那没有这个。 - -王名正 46:13 -对,所以这个是需要完善的。 - -卡若 46:14 -对,所以这个是需要完善的,不能。 - -许永平 46:14 -对,所以这个是需要完善的,应该还没到这一步,但是已经那个已经写完了,就是没有也能写就是了。 - -卡若 46:17 -还没到这一步。 - -王名正 46:18 -但是你今天那个今天写完了,就是没有也能写,就是这个接口能正常通,他这里就没问题,知道吧? - -卡若 46:18 -但是已经那个已经写完了,就是没有也能写就是了,你只要接口能正常通,它这里就没问题,知道吧? - -许永平 46:22 -你只要接口能正常沟通他自己的问题,知道他这个点击进去是可以看到他底下有多少用户,以及这个用户点在哪里,看了什么章节的。 - -王名正 46:25 -他这个点击信息是可以看到他底下有多少用户,以及这个用户点哪里看的什么章节的。 - -卡若 46:25 -它这个点击进去是可以看到它底下有多少用户,以及这个用户点的哪里看的什么章节的?是吧? - -王名正 46:33 -把这个是详情,然后这里的话全平台的一个付款,今日点击的跟今日绑定,这个是属于全平台的。 - -卡若 46:33 -这个是详情,是吧? - -许永平 46:33 -这个是讲解,然后这里的话全平台的一个付款,一个点击,一个今日绑定,这是全平台的。 - -卡若 46:35 -然后这里的话全平台的一个付款今日点击的跟今日绑定,这个是属于全平台的。 - -王名正 46:42 -对,因为这个只有一个看书付款跟分销的功能,跟即时到账的功能。 - -卡若 46:43 -这因为这个只有一个看书、付款跟分销的功能跟及时到账的功能嘛。 - -许永平 46:43 -对。因为这个只有一个看书付款的推销的功能,及时到账的功能,然后有一个的话,他只要向我发给发这个链接,我发到群里面去,这个群里面有 10 个人,点击这个链接就跟你合法关系了,这 10 个人接下来一个月所有付费都跟都会。 - -王名正 46:47 -那有一个的话,他只要叫我发给发这个链接,发到群里面去,这个群里面,然后。 - -卡若 46:48 -然后有一个的话,他只要像我发给发这个链接,我发到群里面去,这个群里面有 10 个人,点击这个链接就跟你捆绑关系了,这 10 个人接下来一个月所有的付费都跟都会。你都会收到钱,那它是这个逻辑,所以,而且是及时的,一付款的话就会及时的到那个我们。 - -许永平 47:02 -你都会收到钱,对吧?但是这个逻辑,所以,而且是及时的,你付款的话就会及时的到那个群,你怎么获取到他? - -卡若 47:14 -群里怎么获取到它?ID,它点击这个小程序是带 ID 的呀。 - -许永平 47:17 -ID,他点小程序的 ID 的呀。 - -卡若 47:21 -是要他们点过才有,你没它。 - -许永平 47:21 -这要他们点过才有,没点我。 - -卡若 47:23 -它点进来就可以了,点。 - -许永平 47:23 -他点进来就可以了。我知道,就比如说你发到一个群里面,群里面有 5 个人,他如果没有去点的话,那点了没办法。 - -卡若 47:25 -就比如说你发到一个群里面,群里面有 5 个人。 5 个人,他如果没有去点的话。要点呢。没办法。点就绑了,我们就拿到。 - -许永平 47:35 -点就把它。 - -卡若 47:36 -跟传统,跟你传统分享出去其实是一个逻辑,跟群也没有关。 - -许永平 47:36 -它这个跟传统的,跟你传统分享出去其实是一个逻辑,跟群也没有关系。 - -卡若 47:42 -分享出去是一个逻辑啊。 - -许永平 47:43 -分享。 - -卡若 47:44 -这就跟你正常发一个人或者发到朋友圈人家去点的逻辑一样,它跟群也没有关系。 - -许永平 47:44 -这就跟你正常发一个人或者发到朋友圈,人家去点的逻辑是一样的,它跟群也没有关系。 - -王名正 47:51 -对呀。 - -卡若 47:51 -对,就是我指群,是群里面人多,你点了就容易嘛,是吧? - -许永平 47:51 -对,就是我只群,是群里面还多一点就容易嘛。就跟发个人一样啊。 - -王名正 47:56 -就跟发个人一样,他只是说把这个链接丢到群里了,然后曝光更多嘛。 - -卡若 47:56 -发个人一样啊。 - -许永平 47:58 -对。 - -卡若 47:59 -你推荐,比如,对,比如远志推荐给。谁谁谁,那这个人他只要点了,接下来他看了觉得有用,看了20%,想继续看,付一块钱你 9 毛钱就到你微信了。 - -许永平 48:03 -这个人他只要点了,接下来他看了觉得有用,接下来的一个月所有看的都跟你有关系,都自动的。 - -卡若 48:10 -而且这个人接下来的一个月的所有看的都跟你有关系,每次九毛都到你手上,知道吗?都自动的,它逻辑是这个,那我是可以看到有多少人绑嘛? - -许永平 48:18 -他逻辑是这个,那我是被看到的,所以你发一个群,比如这个群进去就很容易收费了,而且解还要这个解决另外一个问题,是到账的问题,这有。 - -卡若 48:21 -所以你发一个群,比如这个群 500 个人进去就很容易收费了。而且解,还有这个解决另外一个问题是及时到账的问题,这永平就特。 - -王名正 48:32 -这里面。 - -卡若 48:32 -特别要注意一下,因为这一块我就讲一下这个完善的一些东西嘛。 - -许永平 48:32 -您会特别要注意一下,因为这一块讲一下这个。这个用户详情页你估计得先以这个项目单独跑,先不接。 - -卡若 48:37 -用户详情页,你记得建议这个项目单独跑,就先不接。 - -许永平 48:42 -我单独跑。 - -卡若 48:42 -我单独跑,现在都实现了,我还搞啥呀? - -许永平 48:44 -不是,就是你说的这些用户轨迹,你只能说按你这个项目单独去实现这个功能,你如果要等去接那一块,然后用户轨迹。 - -卡若 48:46 -用户轨迹你只能做按你这个项目,按单独去实现这个功能,你如果要等等去接那一块,哪里那么快?用户轨迹,现在你这个改一下就可以直接用了,我现在是没有去改,知道吧。 - -许永平 48:57 -现在这个改一样,你走那。 - -卡若 49:02 -走那边。的话,我要完善的用户,然后这个交易。 - -许永平 49:02 -那边的话查的是不要完善,然后这个交易就是你先以这个项目名存在这个项目下。 - -卡若 49:09 -项目存在这个项目。现在就是存在这个项目,这个现在就没有问题啊。 - -许永平 49:11 -现在就好了。 - -卡若 49:13 -知道,但是你那个商详页里面。 - -许永平 49:13 -知道,但是你这个详情页里面。 - -卡若 49:15 -就是这里面,就是你要去看一下他有一些逻辑的,哪里还有能优化就优化一下嘛,是吧? - -许永平 49:16 -就是这里面,你要去看一下。 - -卡若 49:23 -包括这里面的一些,我说几个点嘛?他那边错了。 - -许永平 49:26 -他那边做好再去提,不然没用。 - -卡若 49:29 -没有,这个要现在要上线了,他们现在就是。 - -许永平 49:29 -你看这个要现在就是。对吧? - -卡若 49:32 -开始推,对吧? - -许永平 49:32 -所以说你现在你这个项目先存在这个项目,就按你的项目。 - -卡若 49:36 -然后这里的话就是一个交易中心和这个用户管理这边的那个绑定的这一个数据还没有到交易中心,他还没做统计,你看这个是付款的,但是这个还没做统计,你看一下那个接口的问题,这有永平,你对一下,明天挑个时间看一下。 - -许永平 49:36 -然后,嗯,这话就是这个交易中心用那个绑定的这个数据,但是这个还不够,这个就跟你对一下,一定挑个时间看一下。好,你这个。 - -卡若 50:00 -哦,你这个是。 - -许永平 50:02 -是这个就是一些用户管理的一些,那这个内容的我说一下长一点的逻辑核心内容这一块的话,还有一个问题是啥? - -卡若 50:02 -这个就是一些用户管理的一些点嘛?那这个内容的我说一下,你讲一下逻辑就比较清楚,内容这一块的话,还有一个问题是啥?就这里的话它是有免费章节的,设完免费章节前台还没有,这个就变成0,但是前台是没有反应的,前端的这一个它不会变成免费,这个我还没去修,我说几个 bug 你记一下就行了。 - -许永平 50:14 -就这里的话它是有免费章节的,设完免费章节,前台这里变成免费,还是前台是免费前端的这一个它不会变成。 - -卡若 50:31 -那这个文件路径。这个是不需要去想的,因为这个的话它是优先读取数据库的,数据库有问题访问不了,它才会读这个文件,它做了双向的,就这这几个,这几个的一个一个问题,我比较重要的一点是这个用户管理的这一块嘛。 - -许永平 50:34 -这的话他是优先要先读取数据库的,数据库有问题导入不了,他才会,他那个双下的就这几个,对,这几个这一块。 - -卡若 50:58 -现在付款这些是都没有问题了,付款呐什么的。 - -许永平 50:58 -所以他付款,这些是付款的。 - -卡若 51:02 -分销,然后提现呐? - -许永平 51:03 -分享这一个小的网页,这个就不用去网页关系那里。 - -卡若 51:04 -这一个是没有问题,就有一些小的问题,是吗?那网页这个就不用去管它,网页端现在不用去管它。主要还是小程序端。 - -许永平 51:17 -主要还是想听同学,然后这里的话。 - -卡若 51:19 -然后它这里的话我看一下这个,就我现在只会专注写东西,你知道吧。 - -许永平 51:30 -我现在只会专注写东西了。 - -卡若 51:32 -这一块我就不会太花时间了。 - -许永平 51:32 -这一块不会太花时间的。 - -卡若 51:35 -那存客宝接口。加好友。 - -许永平 51:38 -加好友。这加这个的话是确定提现的,这现在是自动,这个要实现的是自动分,就老王这个自动分他确定就对了,一样的,知道吧。 - -卡若 51:39 -接了这个的话是确定提现的,这现在是自动,这个要实现的是自动分,就老王这个自动分他确定就可以了,是一样的,知道吗? - -王名正 51:39 -写了。 - -卡若 51:50 -这个到时候也不接这个账号。 - -许永平 51:50 -你这个到时候也是接那个。 - -卡若 51:52 -他这个就是。 - -许永平 51:52 -他这个就是就完成了。 - -王名正 51:52 -就王子哥可以。 - -卡若 51:53 -关键字段。 - -许永平 51:54 -你把,你把你那个调通,这个也是,对,你们那些,就也是那些什么那个项目。 - -卡若 51:54 -你把你那个,对,你们那些就远志那边,咱们那个项目管。管理的这一些内容,你得统一一个文档管理,这样不会乱。 - -许永平 52:02 -管理的这些内容也一个文档,这样不会乱。 - -王名正 52:06 -那到时候我把这一个 Markdown 文件丢给你,你直接对接吗? - -卡若 52:06 -那到时候我把这一个 Markdown 文件丢给你,你直接对接吗? - -许永平 52:06 -那到时候我把这一个拉个大文件就给你。 - -卡若 52:11 -对,然后它里面有很多的那些那个逻辑,这个是逻辑是什么? - -许永平 52:12 -对,然后它里面有很多的那个逻辑,这个逻辑就是拆解一下,先了解清楚了,就是它这个里面的话能匹配的话。 - -卡若 52:20 -就是拆解一下先,你先了解清楚永平就是它这里面的话像匹配的话像这个。 - -许永平 52:32 -他这个第一个匹配的话,就是直接匹配,匹配数据库里面的一些用户,注册的用户,有危险的用户,注有手机的用户,有重要的手机的用户。 - -卡若 52:34 -第一个匹配的话就是直接匹配,匹配数据库里面的那些用户,有注册的用户,有微信的用户,有注有手机的用户,因为这个没有绑手机,所以匹配不到。 - -许永平 52:45 -那第二个匹配的话是他,那个他需要填资料,就是匹配相应的关键字。 - -卡若 52:45 -第二个匹配的话是他那个他需要填资料的,就是匹配相应的关键字就是你的,你的标签是做私域的,那你匹配找资源就会找私域的,或者我能解决私域问题,他填的。 - -许永平 52:54 -就是你的标签是做私域的,那你匹配找资源就会找私域,或者我能解决私域问题,他。填的是私域的管理度相关的,匹配一下,那后面这几个的话,匹配完之后就直接联系方式了。 - -卡若 53:02 -是私域两个关键字或相关的关键字,它就会匹配相关的,那后面这几个的话就是匹配完之后就直接留联系方式了,像这个他就直接调这个里面的这一个微信,那你确定之后就存客宝就加过去了,对,是我的,我那个号就加过去了,和这个也是一样的,这些都是要检查一下吧? - -许永平 53:11 -而且这个他就直接调一个里面的这一个微信,那你确认之后就是多少价格。这个也是一样,这些都是要检查一下,给自己一点提醒,那这个后端是因为现在是漏了。 - -卡若 53:26 -远志也去检查一下,那这个后端是没有问题的,现在是弄的是我。 - -许永平 53:32 -是卡若这个号去抢,只要他们匹配一下。 - -卡若 53:32 -卡若一个号去加,只要他们匹配就直接加了。所以存客宝那边的稳定性是那个需要去弄一下的,那这个我因为我肯定是要赶紧上线的,我明明天可能就要让他们直接用,肯定会出很多的问题的,到时候你群里面都一定会有。 - -许永平 53:38 -所以陈哥把那个权限是那个需要去弄一下。好。那这个我肯定是要赶紧上线,明天的就要让他们直接用。因为出很多的问题,到时候。都还没,都搞定。 - -卡若 54:04 -统计啥? - -许永平 54:04 -统计了。 - -卡若 54:05 -资金。 - -许永平 54:06 -资金。 - -卡若 54:06 -这个不用资金统计,他们能拿到钱、能分发、能有人付钱,能拿到钱就行了。 - -许永平 54:06 -这个不用资金统计,他们拿到的协议就是他能有的资金。厨房也没,也还没实现。 - -卡若 54:13 -分钱已经实现了,分钱他能直接实现,我刚刚就一块钱,你们回头自己付一块钱,就知道你转给永平,永平付一块钱你就能看得到了。 - -许永平 54:13 -分钱已经实现了,分钱他能直接实现,然后用一块钱,不要自己提一块钱,提现了你才可以。有没有一块钱就能看得到? - -卡若 54:23 -提现是在这边推广中心这里。 - -许永平 54:23 -提现是在这边,对完公司这。后台那个资金统计不是。 - -卡若 54:27 -不是。因为这个还没对,这个老王协助一下永平,对一下就好了。 - -许永平 54:29 -因为这个还没对多少,然后写你里面。 - -卡若 54:31 -你明天几点要? - -许永平 54:32 -今天要跟他们说可以使用啊。 - -卡若 54:33 -我明天早上 6 点我就开播,就说了就是这个提现不能再拖了,已经拖了一个月走了,我不可能这个流程已经通了,我也让他们跑过了这个分论分销跟登记统计的,你们就把修补一下就好了,他们核心一点就提现能,就像。 - -许永平 54:34 -我明天早上。那么早说了,咱们等下测试。 - -王名正 54:38 -就是这个理解,不能再拖了,因为拖了一个月流程已经通了,而且刚刚跑过了这个分店分销跟登记统计的,你们就把申请提现,你。 - -许永平 54:39 -就是这个洗一下,我跟你说是这个,我觉得不能再拖了,别改。基础流程应该都没问题的,应该是可以。流程已经这个分本分销。如果这些是可以的话,那基本是没什么问题。 - -卡若 55:02 -那你的小程序是一样的,一个提现,一个统计。 - -许永平 55:02 -企业下面小程序是一样的。 - -王名正 55:04 -对,你提现,现在,不,你现在提现不着急,你可以先让他们用完之后,我们然后之后再发一版。 - -许永平 55:04 -对,企业现在是一个。现在不着急嘛。 - -卡若 55:07 -不着急,你可以先让他们用了,用完了之后我们。 - -许永平 55:11 -对。 - -卡若 55:11 -不提现,直接用,因为这个就是你把你刚刚弄的那个功能接上去就结束了,知道吧? - -许永平 55:13 -极限直接用,因为这个是你把大家都弄,因为我们接上去就结束了。 - -王名正 55:14 -因为这个就是你想让他弄就接上去就结束了,你知道我已经最新的,我已经上到他。 - -许永平 55:19 -好。 - -卡若 55:19 -我已经是最新的,我已经上传到 GitHub 上面了。 - -许永平 55:19 -我已经是最新的,我已经上传到 e 卡上面。 - -卡若 55:22 -主要是你得审核。 - -王名正 55:22 -主要是你得审核,那小程序也要审核啊。 - -许永平 55:22 -主要是你得审核。不审。 - -卡若 55:25 -不审,现在不审。 - -许永平 55:26 -现在不审。 - -卡若 55:26 -小程序没有审的吗?小程序这个审核都秒过了。 - -王名正 55:28 -小程序这个简单。 - -许永平 55:28 -小程序这个审核都秒过了。 - -王名正 55:31 -没。我拉一个就秒,客户类型不一样。 - -卡若 55:34 -你那个,你那边,你是不是它本身就有支持不同商户的处理嘛? - -许永平 55:34 -你那个,你那边是不是本身具有支持不同商户的处理? - -王名正 55:41 -什么事? - -卡若 55:41 -因为这是两个不同的项目,你肯定是要跟不同商户的处理。 - -许永平 55:41 -因为这是两个不同的项目,是要跟不同商户的处理。 - -王名正 55:41 -不知道。 - -卡若 55:46 -不用,直接跟我们同一个。 - -王名正 55:46 -没有,不用同一个,对,你后台有弄的话就有像你那个接口写好了之后我发起提现,我数据库记录了,那就可以了。 - -许永平 55:46 -不用。 - -卡若 55:47 -同一个资金库。 - -许永平 55:47 -你拿到资金户。 - -卡若 55:48 -对,同一个就行。 - -许永平 55:48 -对呀。OK。 - -卡若 55:49 -那到时候对,那个明细发那个,对。 - -许永平 55:50 -他到了对应的明细。 - -卡若 55:52 -后台都有。 - -许永平 55:53 -后台他就像你那个接口是要发起提现数据不记录了。 - -卡若 55:55 -像你那个接口是这样,我说我发起提现,我数据做记录了。 - -许永平 56:01 -对啊。 - -卡若 56:01 -对,你到时候。 - -许永平 56:02 -到时候发起一些这种教育。 - -卡若 56:04 -我就给你们先。 - -许永平 56:04 -我是给你们信号,是吧? - -卡若 56:05 -这个字段。 - -许永平 56:06 -你看我这个 6 点就付款,这么大,哎,那点一下链接。 - -卡若 56:06 -你看我这个谁 6 点又付款了,一个一笔,哎哎,就是他是及时很重要,知道吗? - -王名正 56:07 -对,看不大。 - -许永平 56:15 -就是他是及时很重要,知道一个是我这边有付款,我这里他是主号,我一定会收到的。 - -卡若 56:17 -一个是我这边有付款,我这里的话是属于主号,我一定会收到的,你看是及时性,一有人付款他买了第几节我是立即知道,这个都不知道谁买的,对吧? - -王名正 56:17 -一个是我这边。 - -许永平 56:24 -你看看是及时现一有人付款,他们的第几节不是你知道都不知道谁买。 - -卡若 56:31 -我就收到这个钱嘛。 - -许永平 56:31 -是吧?我就收到这个钱。 - -卡若 56:32 -那比如我把这个链接给老王,知道吧? - -许永平 56:32 -那如果我把这个链接给老王,叫老王去付款,我这里收到钱了,好,钱。 - -卡若 56:36 -老王付一付款了,我这里收到 9 毛钱。诶,这实现不了,现在不是提现得自己,这个是相当于转账的功能,就不是那个提现。 - -许永平 56:40 -诶,这个实现不了,现在不是提现给自己,这个是相当于转账,不是那个提现。 - -卡若 56:46 -提现功能,这个是。提现在是要手动点的。 - -许永平 56:47 -提现是这样的,手动点的就没有自动。 - -卡若 56:49 -就是手动点,就刚刚这个过程跟老王那个一样。 - -许永平 56:50 -就刚刚这个过程,这应该是要调调转账的功能。 - -王名正 56:54 -他再一个是商户卡若是商户给他管理者,所以说有人付款商户的都能及时收到通知。 - -卡若 56:54 -他在一个是,他是卡若的管理者,所以说哦。 - -许永平 56:54 -他这一个是。看那个。 - -卡若 57:01 -他那个。那个分账的也是一样。 - -许永平 57:02 -那个分账收款记录不是分账。 - -卡若 57:04 -不是分账,对,他这是收款。 - -王名正 57:04 -对,这收款记录。 - -许永平 57:05 -对,他这是收款。 - -卡若 57:07 -分账的也是一样,就刚刚老王那个碎片时间的事情。 - -许永平 57:07 -分账也是一样。账和分账时间。账是现在要自己点。 - -卡若 57:10 -大家自己点对,自己去提现,然后就是领取。 - -王名正 57:11 -对,是。 - -许永平 57:12 -对。自己去提现,然后自己去领取。 - -卡若 57:14 -就是我刚刚的那个界面,他点了确定了他就到账了,知道他要需要自己点,就这个流程就跑一下,反正他们我明天会直接跟他们讲去,因为太久了一直在说,对吧? - -许永平 57:14 -就是我刚刚的那个界面,他点了确认他就到账了。对,他要自己点。大家要自己点这个流程跑一下,反正他们我明天会直接跟他们讲去,因为太久了,一直在说。好,就都那个群里,你看都。 - -卡若 57:30 -都那个群,你看都多少个人?真的嗷嗷待哺,他们天天培训,天天培训,培训没结果,很快就没士气了。 - -许永平 57:32 -找个人,然后再给他们培训,没结果,太磨士气了。 - -王名正 57:33 -OK。 - -卡若 57:37 -知道,我现在都是以让给他们补贴的,硬给的。 - -许永平 57:38 -是吧? - -王名正 57:38 -我现在都是让给他们对比,那这个已经基本上线,让他们去做分发就行。 - -许永平 57:38 -我现在就是让给他们补贴的,定点的。好。 - -卡若 57:42 -知道吧? - -许永平 57:43 -知道吧? - -卡若 57:44 -那这个已经基本能上线了,就让他们去做分发就行了,还有里面还需要一个逻辑的,也是要整理一下,就是接下来每我每新增一章节,我把这个过一下。 - -许永平 57:44 -那这个已经基本上线了,让他们去做分发就行了,还有里面还需要一个什么要整理一下,就是接下来每我每新增章节把这个过一下,明天 6 点肯定不能说明天的话,明天我们都还。 - -王名正 57:49 -那你里面还需要一种,嗯,就是接下来产品每新增一章节。 - -卡若 57:59 -明天 6 点肯定不能说别人的话,明天我们都把电话。 - -许永平 58:02 -用不直接用,先用出问题再说,现在是不管跟长能看到前情况明显出问题再解决就行了。 - -卡若 58:03 -不直接用,出问题再说现在是付款跟分账的,能看得到钱就行了,提现出问题再解决就行了。 - -王名正 58:03 -不用不用,之后你再说现在是试管。现在是试管。我再让他们想办法。 - -卡若 58:11 -我让他先让他们小规模测试,不要每个人都用。 - -许永平 58:12 -我让他先让他们小规模测试,不要每个人都用,还有我刚刚说啥来着? - -卡若 58:16 -诶,我刚刚要说啥来着? - -王名正 58:16 -他说他自动的接一下。 - -卡若 58:17 -那数据统计那块没用,那到时候数据一统计清掉。 - -许永平 58:18 -那数据统计那块没弄,那到时候数据你也得重新清掉。 - -卡若 58:21 -没有,它都自动地去分账,你管数据统计这个。 - -许永平 58:21 -不是,他都自动的去分散。不是后台的那个统计,你现在这个资金统计不是没有吗? - -卡若 58:25 -现在这个自己统计没用吗?这个就接一下嘛? - -许永平 58:28 -这个接一下,这个在就是统计这一块的。 - -卡若 58:28 -你们看一下这个跟就是统计这一块的嘛。对,那你给。 - -许永平 58:31 -对,那你。给他们用完,到时候数据对不上你就得清掉。 - -王名正 58:35 -那钱就直接现在知道怎么办了,一个人跑店的,一个人挣 9 毛钱也行。 - -卡若 58:35 -那钱都直接给他们了,现在不是自动分账了吗? - -许永平 58:35 -那钱呢?直接给他,这样不是自动分账了吗?不知道,就是。 - -卡若 58:41 -比如这里绑定这一个人,这 9 毛钱就直接那个嘛? - -许永平 58:41 -比如这里绑定这一个人,这 9 毛钱就直接点,你把这个。 - -卡若 58:45 -你把这个我明天肯定是先让他们去走,去发钱,然后里面有点钱。 - -许永平 58:47 -就是到你测过的话。 - -王名正 58:48 -我明天。 - -许永平 58:48 -我明天肯定是先让他们去,走,去花钱,然后这边说内部钱灰色的这种,对,先测一下,测的有问题再说吧。 - -卡若 58:53 -对,先测一下,出问题再说,这是一个,因为这个事情有一点是。 - -王名正 58:54 -我先测一下再说这几个,因为这个事情有一点是。 - -许永平 58:58 -再说这是一个因为这个事情有一点是。 - -卡若 59:04 -那个的他也就离变现比较近,直接就用就行了,对吧? - -王名正 59:04 -那个,那你这是比较近。 - -许永平 59:04 -那个,嗯,他以就离变现比较近,直接就有就行了,对吧? - -卡若 59:10 -然后里面有一些小小的一些逻辑,你们拆解完之后去看一下那个文档,自己去拆去看一下那个文档吧。 - -许永平 59:10 -那里面有一些小的线路,你们拆解完之后去看一下那个文档,自己去拆看一下那个文档。 - -卡若 59:18 -然后我刚刚还有一个啥事,对了,这里的话是新增章节,我会新增一些章节,比如新增一块这个数就会变,这个是另外一个隐藏的版块了,新增一个那个数就会变成 9 块9,比如今。 - -许永平 59:19 -哦,对了,这里的话是新增章节,我会新增一些章节,比如新增一块,这个是另外一个隐藏的板块,新增一个,那个数据会变成 9 块9。 - -王名正 59:19 -对,这里的话是新增章节,我会新增一些章节,新增这数字变这个是另外一个,新增一个,那个数字会变成 9 块9。 - -卡若 59:32 -挣一块就变 10 块钱,我多写一张就变 11 块钱,但是 9 块 9 是现在 62 张,小结是相当于是基础版的 9 块9, 62 张后面新增一块。 - -许永平 59:34 -我多写一张就变 11 块钱,但是 9 块钱的现在 62 张奖金是相当于基础款 9 块钱, 62 张后面新增一块。 - -王名正 59:36 -但是 9 块钱变成62。也是。62。 - -卡若 59:44 -跟章节有关系,跟你里面的这个价格。 - -许永平 59:44 -跟章节有关系,跟你里面的这个价格没关系。 - -卡若 59:48 -跟价格有关系。 - -许永平 59:48 -跟价格就你多一张就多一块钱,我可以设成两块,我多 100 块。 - -卡若 59:49 -就你多一张,就多一块钱嘛。我可以设成两块就多两块钱,我多 100 块就多 100 块钱。 - -许永平 59:55 -那就是加上你新增的这个章节的总共多少钱嘛? - -卡若 59:56 -加上你新增的这个章节的多少钱呢? - -王名正 59:59 -来一个,是。 - -卡若 59:59 -它就等于一个是普通版。 - -许永平 01:00:00 -它不等于一个是多。 - -卡若 01:00:02 -一个是增值版,买普通版还是 9 块9,买增值版的就是 9 块 9 的不断的叠加。 - -许永平 01:00:03 -那是增值版,买普通版的是 9 块9,买增值版的。 - -王名正 01:00:04 -来,我们把。 - -卡若 01:00:09 -叠加的金额是不是你新增章节的金额吗? - -许永平 01:00:10 -加的金额就是新增的发票金额。对,就这一个,就有一个普通版的那个增值版。 - -卡若 01:00:14 -对,新增章节的金额就这一个,就有一个普通版,一个新那个增值版的。 - -王名正 01:00:22 -对。 - -卡若 01:00:25 -那你这里面也没有分普通版跟增值版。 - -许永平 01:00:25 -那你这里面也没有分普通版和增值版。 - -卡若 01:00:28 -还没分,但这个你们记要就要记一下这个事情。 - -许永平 01:00:28 -还有一个就是要,就要记一下。 - -王名正 01:00:30 -你们说专把。 - -许永平 01:00:32 -事情。 - -卡若 01:00:38 -目前两个逻辑都。 - -许永平 01:00:38 -目前两个逻辑都没有,对吧? - -卡若 01:00:40 -哪个逻辑已经有那个,但是他这个是看了十章之后才会触发这个增值的这一个内容。 - -许永平 01:00:40 -哪个逻辑已经有那个,但是他这个是看了十张之后才会触发这个增值的这一个点。 - -王名正 01:00:41 -因为有那个。 - -卡若 01:00:50 -就比如匹配三次就要让他填手机号码了,他可以免费匹配第三次,他就要自己填手机号码这一些里面的一些和那个。 - -许永平 01:00:50 -就比如匹配三次就要让他填手机号码,他可以免费匹配第三次,他就要自己填手机号码这些里面的一些那个。 - -王名正 01:00:55 -他可以免费。分析里面的一些那种,那个。 - -许永平 01:01:02 -那个步骤跟逻辑,还有一些小型的算法在这里面就匹配三次,所以然后他付款买了一张才能匹配资源,这里面猜一下就比较清楚一点,或者是现在的话,一个极限的话可以快速记一下极限,记得这个快速了解一下这个事情。 - -卡若 01:01:03 -步骤跟逻辑,还有一些小型的算法就在这个里面,比如匹配三次之类的,然后他付款买了一张才能匹配资源,这里面你们猜一下就会比较清楚一点,或者现在的话一个提现,老王那边就快速地对一下提现跟统计的这一个点,跟永平就快速地了解一下这一个事情,这一个是。 - -王名正 01:01:22 -这个是现在的一个极限。 - -许永平 01:01:32 -这一个是为什么? - -卡若 01:01:32 -为什么让你们快速过一下这个事情呢? - -许永平 01:01:33 -让你快速过一下我的一本书单位开完会,这也是可以,这个机构的文档是一样的。 - -王名正 01:01:34 -他是那个。 - -卡若 01:01:35 -他现在用在的地方很多,现在只是我的一本书,对吧?我们昨天刚去那个银掌柜那边也开完会,这个也是可以应用在金融方面的,就我丢一个金融的文章过去解决获客给他们中台,那就不用,不一定买手机了,是不是一样的逻辑? - -王名正 01:01:56 -都一样,逻辑性一样。 - -卡若 01:01:58 -是一样的,是吧?你看。看一些金融的一些视频,或者一些干货一样的,是吧? - -许永平 01:02:06 -然后这个要记一下,看把这个进度,而且可以在对这些的时候不考虑一下这方面,对才会发现。 - -卡若 01:02:12 -这,这个就要记一下,看那个嘛?你把这个进度每次就新增一条,有一个进度条嘛。 - -王名正 01:02:18 -你看。 - -卡若 01:02:20 -我们就几条线,可以这几条线直接往下去弄,然后永平在对这些的时候,你就多考虑一下审社所这方面的事情,你对着就才会发现哪里有问题嘛。 - -许永平 01:02:32 -好。 - -卡若 01:02:33 -对吧?因为我们一定是那个,包括老王,也是你在对碎片时间,对这个数据接口的过程当中碰到问题就直接解决掉。 - -许永平 01:02:33 -那我们一定是,包括老文也是在对碎片时间,对。 - -卡若 01:02:44 -问题肯定有的,你看远志今天铺这个问题不就出来了吗?那我们把它解决掉就行了,对吧?你这个机器的这个基础的那个配置,配置完之后是就会有很多的就直接复制就行了,是吧? - -王名正 01:02:52 -第一次。 - -卡若 01:03:02 -就不会太多,有很多的参考的一些东西,是吧? - -王名正 01:03:03 -就不会。所有的参考。 - -卡若 01:03:08 -包括这个我再简单地说另外一个事情,那个我发了一个会议纪要的那个通版的那个原则。 - -王名正 01:03:13 -那个要这个,但是不一样,这次不一样,会自动。 - -卡若 01:03:19 -在哪?我就在这个 NAS 上面,这个。这个是智能纪要的,这个智能纪要会自动,你总结完就自动发到群里面了,我。过一遍。 - -许永平 01:03:33 -懂的话。 - -卡若 01:03:33 -就是 V0 的那个要考核的吗? - -许永平 01:03:34 -他来不出答案。 - -卡若 01:03:35 -这个就变成固定版了。看一下。我给你过一下,这个你就比较清楚,对,你们直接对完那个,直接那个嘛。 - -王名正 01:03:41 -我让那些配置参数先码给。 - -卡若 01:03:47 -嗯,我看一下,就昨天的,我举个昨天的,因为你在 cursor 上就能直接解决,我看你 V0 不太喜欢用。主要是它加载不出来。对吧?找了密钥,怎么了? - -许永平 01:04:03 -咋了? - -卡若 01:04:04 -打开了。 - -王名正 01:04:05 -打款的。 - -卡若 01:04:08 -那我。你这个发到群里可以发出去,你发到群里了。 - -许永平 01:04:12 -可以。 - -王名正 01:04:13 -我发你个人了。 - -许永平 01:04:15 -发到群里了。 - -卡若 01:04:16 -发群里了。发出去是发出去了,这个怎么能发到群里? - -王名正 01:04:17 -发群里了吗?是发群还是发个人了? - -许永平 01:04:18 -你发错了,撤回一下。这个怎么能发到群里?去找一下,还有其他的吗? - -卡若 01:04:23 -群里面还有其他人吗?没有,我们这群没其他人。 - -许永平 01:04:25 -没有。 - -卡若 01:04:27 -没有。 - -王名正 01:04:27 -没有。 - -卡若 01:04:28 -自己人没事,有其他人就别发了,我。 - -许永平 01:04:29 -那就行。我想知道。千文还有。 - -卡若 01:04:32 -我把那个我给你过一下这个东西,比如这个昨天永平开会的嘛。 - -王名正 01:04:33 -我那个这种。 - -许永平 01:04:34 -那个你可以过一下。 - -卡若 01:04:38 -你说的那个是 cursor 的生成会议纪要的内容。 - -许永平 01:04:38 -你说的那个是 cursor 的生成会议纪要的。 - -卡若 01:04:42 -不止生成会议纪要,我跟你们讲有两个事情,不然我不会拎出来讲。生成会议纪要那就太简单了,这一个是我放到这随便拉一下,你看发送那个,诶诶? - -许永平 01:04:49 -这个是好的啊。 - -卡若 01:05:01 -没拉过去拉。 - -卡若 01:05:09 -诶,你这是飞书链接吧?不,我,我跟你讲,等一下,我先拉过来。上面那个是啥? - -许永平 01:05:17 -方面的一个思考。 - -卡若 01:05:18 -等一下你把这一个生成总结纪要,然后发到飞书的这个群里面来,我简单地说一下。个,这个是飞书的这个Webhook。 - -王名正 01:05:35 -那个。 - -卡若 01:05:38 -就拉了一个小机器人嘛。对,就这里面,你可以,这里面微信也可以实现的。我讲这个是这一个逻辑,跟我们这个现在存客宝功能有用的,这里的话是配置的一个那个 Webhook 的地址。知道,那我这个做 Webhook 的地址做完之后是干嘛呢?我现在是不是把这个智能纪要这个弄完了? - -王名正 01:06:03 -是不是? - -卡若 01:06:06 -然后这一个就总结智能纪要,总结纪要,然后就回测一下,它就生成直接发到群里面去了。 - -王名正 01:06:12 -一样。 - -卡若 01:06:20 -知道,那这个是用来干嘛的呢?先,你们先看一下流程,会比较清楚。微信应该没这种功能吧?微信有我,我们存客宝也可以做。 - -王名正 01:06:29 -不不不。 - -许永平 01:06:31 -你说事啊。 - -卡若 01:06:32 -这个的功能发到个人的群也可以做这个功能的。 - -王名正 01:06:32 -产研团队每日会议, 2026 年开会前必看。无文档不开会,会前请大家按照模板填写要讨论的内容,踊跃评论,积极讨论。 - -卡若 01:06:37 -这现在没有去做嘛?我先把这几个字提到的你们就记一下,不然回头都会忘掉,知道吗? - -王名正 01:06:40 -准时开始后集体默读 10 分钟, 10: 00- 10: 10,请所有人踊跃评论,这也会被。 - -卡若 01:06:43 -这个就是要直接加到一些需求里面去,我先说一下这个思路是怎么样的? - -王名正 01:06:44 -产研团队每日会议, 2026 年开会前必看。无文档不开会,会前请大家按照模板填写要讨论的内容,踊跃评论,积极讨论。 - -卡若 01:06:48 -这个智能纪要它做完之后,它就自动的是把这个发到这个飞书群里面去,那发的样式是怎么样? - -王名正 01:06:52 -准时开始后集体默读 10 分钟, 10: 00- 10: 10,请所有人踊跃评论,这也会被。 - -卡若 01:07:01 -像这个吗?我给你看一下,他们现在也那个发出来的样子,就长这样,嗯,他就会应该受,咋会跑到这里去? - -王名正 01:07:07 -微信的那个发出来的样子。 - -卡若 01:07:19 -什么鬼?一条消息?那这个可以做到什么程度? - -王名正 01:07:26 -这个可以做到。 - -卡若 01:07:27 -就是我们开完会之后触发自动地去把。这个内容发出去。 - -王名正 01:07:34 -发出去,不需要,那这个。 - -卡若 01:07:36 -那你前面那些下载不是手动的吗? - -许永平 01:07:36 -前面那些下载。 - -卡若 01:07:38 -不,不需要,我给你看看,做完了你们打开群看一下。比如说前面几步不得是手动吗? - -许永平 01:07:48 -就前面几步都给手动,他就会是。 - -卡若 01:07:49 -不需要,我等一下会说,我只是为了展简单演示,告诉你们它的实现流程,是吧?它这个图片,没没没发图片,没发你就正把图片也发过去。通过接口把会议图片,我说一下,这个要配置那个TOKEN,所以我才没有先给你们过一下,这一个就比较清楚,是吧? - -王名正 01:08:05 -对。没错。 - -王名正 01:08:18 -我说一下这个在配置那个。 - -许永平 01:08:21 -对。 - -卡若 01:08:33 -他这个是昨天的一些总结, 12345 的,他还会有一张图片出来,那这个我已经放到那个里面去了。 - -王名正 01:08:33 -那这个是我已经发到这里了。 - -卡若 01:08:42 -工作流吗?对,他是一个工作流,这个是基础的一个一个事情,那他可以做到的一点是什么? - -王名正 01:08:45 -那是。那他可以做到。 - -卡若 01:08:50 -就是咱们这边不是有那个会议的图吗? - -王名正 01:08:53 -反正这边不是那个会议的,那。 - -卡若 01:09:05 -就比如我们开会的文档,你直接丢过去就可以了,来,我给你们过一下就知道了,要一步步,不然一定会乱,那么他这个有点不一样。 - -王名正 01:09:08 -跳过直接贴过去就行。 - -卡若 01:09:25 -那不也要用客色吗?手动的。你这一步不也手动吗? - -许永平 01:09:29 -这也不也是手工吗? - -卡若 01:09:30 -自动发旅客色吗? - -许永平 01:09:31 -能自动发客户? - -卡若 01:09:33 -当然是可以的,我是一步步跟您讲的,不是您才知道实现流程,不然会有那个的这个要应用到其他地方去,就是现在就把这个链接里面的这一个文字文档导出来,然后发到飞书里面,发到那个会议纪要的飞书里面,然后全部用命令行,不要出现那种扫码的一个一个形式,然后你直接去获得那个相应的 API 跟TOKEN。 - -许永平 01:09:33 -当然是可以的,我是一步步想才知道自己成功了。 - -王名正 01:09:40 -这个要应用到其他地方去,就是现在这个预约。 - -王名正 01:09:56 -不用命令。 - -卡若 01:10:03 -就大概这样,好吧?你给他回,他就会去找链接,自己链接里面文字找出来。 - -王名正 01:10:08 -那就会去找链接,链接就对。 - -卡若 01:10:12 -那这一步,这一步你要怎么去实现自动化? - -王名正 01:10:12 -那这一步你要怎么去实现这个? - -许永平 01:10:12 -那这一步,这一步要在执行那个会议丢到科室里面去执行这种。 - -卡若 01:10:15 -哪一步? - -王名正 01:10:15 -诶,你说开那个会议室吗? - -卡若 01:10:15 -你说。开执行的这一步。开那个会议,是吗?就那个会议丢到 cursor 里面去执行这个东西。 - -王名正 01:10:19 -这个会议丢到科室里面去执行这个东西。 - -卡若 01:10:22 -对,这个就是会议,那这个就非常更简洁了,这一步一步来嘛,你就现我们把每天的这个,你抓到这个产研团队会议纪要。 - -王名正 01:10:22 -对呀,这个就是会议,这个就非常更简洁了,这一步一步来,你在线,我们把每天的这个,你抓到这个产研团队会议的。一样的一个内容,开会的这个通过飞书的这个API。 - -卡若 01:10:32 -的这一个内容,开会的这个通过飞书的这个API。他那个。 - -王名正 01:10:38 -他那个是多久啊? - -许永平 01:10:38 -他那个的内容。 - -卡若 01:10:38 -你听我讲完,然后把这个内容,那个视频会议最新的有产研团队会议的内容,把它那个发送到这个,那个解析成最新的这个链接,然后发送到总结成那个会议,发送到那个飞书群里面,那一定要带图片,然后就让他去执行就行。 - -王名正 01:10:42 -会议最新的那个会议的内容,那个发送到这个,那个系统最新的这个链接发送到,总结成这个会议,发送到飞书群。 - -许永平 01:10:47 -发送到这,最新的这个,立即发送到这个。 - -卡若 01:11:03 -诶,咋没看到这个,知道吗?那你每一天开会的,当天开会的产研团队的内容,并且内容要超过 5 分钟以上的内容才执行这个步骤,那全部用命令行实现,不要用网页和这一个,然后把这个 schema 更新一下。 - -王名正 01:11:06 -那你每天开会的当天开会的产研团队的内容,并且内容要超过分钟以上内容才执行这个步骤,那全部用便利贴实现,不要用网页和这一个,然后把这个细节的更新一下。 - -卡若 01:11:31 -就大概是这样。 - -王名正 01:11:32 -是这样。 - -卡若 01:11:39 -我就走一下过程,因为这个我已经做完了,所以这一些就是你迭代进去的,就是我们每天开会它是自动去捕捉,开完会自己去捕捉这一些东西,那这个只是基础的一些事情,我在那它这个就做。 - -王名正 01:11:40 -我就走一下过程,因为这个我已经做完了,所以这一些就是迭代进去,就是我们先开会,但是自动去组织,开完会自己去组织这个这些东西,那这个只是基础的一些事情,我觉得。 - -卡若 01:12:02 -做成做完的吗? - -王名正 01:12:02 -就做完了,他没有做成图片的形式。 - -卡若 01:12:06 -过一下吗?但这个他没有做成图片的形式,这个可以做成 html 的图片的形式,因为他这个是总结的行动项,简要的行动项,那你可。 - -王名正 01:12:23 -这个可以做成 html 的图片的形式,因为它这个是总结的行动项,简要的行动项,那。 - -卡若 01:12:32 -可以变成什么? - -王名正 01:12:32 -也可以变成什么? - -卡若 01:12:33 -变成那个会议纪要的形式就可以了。 - -王名正 01:12:33 -变成那个会议纪要的形式,现在是写了一个,你可以还变成会议纪要的形式,因为这个有个总结文档,一个会议纪要那个工作也我也丢进去了,真的。 - -卡若 01:12:36 -他现在是截了一张图过来了,你可以把它变成会议纪要的形式,因为这个有个总结文档,一个会议纪要那个工作流我已经丢进去了,你们自己去在 NAS 上面,那你不用那么智能也可以了,就是生成完之后直接丢就好了,我只告诉你它是可以。 - -王名正 01:12:55 -那你不用那么智能也可以啊,就是生成完之后直接丢入就好了。我只告诉你他是我的意思就是说你。 - -许永平 01:13:01 -是我们。 - -卡若 01:13:03 -手动去开 cursor 去执行,对吧? - -王名正 01:13:03 -手动去打开搜索去执行这个东西,我只是说把之前的几步,你现在变成了。 - -许永平 01:13:04 -去各自去执行,是说只是说自己。 - -卡若 01:13:09 -打开。说只是说把之前的几步现在拼成一步。对,打开 cursor 肯定是要打开的。 - -王名正 01:13:15 -对,那我的意思是说你的,我以为你说的是全自动。 - -许永平 01:13:15 -对,那我的意思是说,我以为你说的这种全自动的。 - -卡若 01:13:15 -那我的意思是说你能,我以为你说的是那种全自动的。那全自动的肯定是能实现的,但是你部署很麻烦的。怎么不能实现全自动的?早就能实现了,就是有些东西我说得太抽象,你们会有很多卡点。 - -王名正 01:13:26 -就是有些东西我说得太粗,你们会有很多。 - -许永平 01:13:30 -说的太抽象。 - -王名正 01:13:32 -有卡点让你们去做,现在都稍微你们现在的一个。 - -卡若 01:13:32 -卡点知道吗?你如果去做这个,现在都我给你,稍微,每天我都给你们分享一点嘛? - -许永平 01:13:35 -如果去做会需要。 - -卡若 01:13:40 -你们就知道一些东西,那这个会议纪要就你快速地去弄一下就知道了。那这个这什么鬼,对吧?他自己会去实现一些东西,自己去解决这个问题,不管他,但有一点是什么,你们现在有一个。问题就是像你刚刚说的,自动的我给你看,自动的去分发,就是我给我自己的微信发条信息,他自己去解决所有问题,是吧? - -王名正 01:14:03 -问题是像你刚刚说的自动的。 - -王名正 01:14:18 -自动地去分发,但是我给我自己微信发消息,解决这个问题,我自己去执行了,也不用那个嘛,给你之前还是那个。 - -许永平 01:14:18 -自动机器人的那个。 - -卡若 01:14:24 -自己去执行嘛,你不是那个嘛?那我给你看一下。机器人的那个,是吧?比机器人那个先进多了。过了,那个什么鬼玩意? - -王名正 01:14:32 -产研团队每日会议, 2026 年开会前必看。 - -卡若 01:14:36 -你不是用他的那个模型吗?他的是抄另外一家的,有几家的不一样,那像这个看到没有? - -王名正 01:14:39 -无文档不开会,会前请大家按照模板填写要讨论的内容,踊跃评论,积极讨论。 - -卡若 01:14:47 -这个消息中枢是干嘛的呢? - -王名正 01:14:48 -准时开始后集体默读 10 分钟。 - -许永平 01:14:50 -是,行。 - -卡若 01:14:50 -就是我自己给微信发信息,我给我自己的微信发信息,我这没装嘛? - -王名正 01:14:55 -10: 00- 10: 10 请所有人踊跃评论,这也会被。 - -卡若 01:15:01 -我给我自己的微信。 - -许永平 01:15:01 -没有自己。 - -卡若 01:15:02 -发信息他自己会去执行所有的动作, Whatsapp 也可以,微信也可以,就所有的都可以,包括网页。 - -王名正 01:15:09 -OK。 - -卡若 01:15:15 -那我就给你们过一下,这个是我给他发信息是发干嘛呢?也可以语音微信,我给他发信息之后,先说一下后面的那个逻辑,他这里这边他会自己去招募人,你知道吗?我给他发信,因为他能力越来越多,他自己去招募团队了,你看每个团队的一个能力和人设,他自己会去定义,这个不是我弄,他自己去生出了几个崽出来。 - -王名正 01:15:45 -这个比如说我让他自己去搜索一个产品。 - -卡若 01:15:50 -就是我上次看他们的那个逻辑,其实就是他,你的下面其实要有一个AI,然后这个 AI 再去帮你。 - -王名正 01:15:54 -当你的上面要有一个。 - -许永平 01:15:56 -你要有一个AI,然后这个 AI 再去。 - -卡若 01:16:03 -控制你下面团队,这还要多一层,就是中间有个 AI 去帮你做所有的事情。 - -王名正 01:16:04 -你下面。对。没有这个意思,就是中间有个这样去做所有的事。 - -许永平 01:16:12 -那肯定这个就是一个AI,它这五个管理就管理每一个。 - -卡若 01:16:12 -那肯定这个就是一个AI,它这五个管理每个人都有,你把它变成就是。 - -许永平 01:16:19 -如果就是它上面其实还有一个管理员,然后它底下就是管理的几个管理层,对,管理它也有自己的记忆。 - -卡若 01:16:20 -其实还有一个管理。管理员,就这个就是大的管理员,他底下就是几个。几个管理嘛?几个管理嘛?那几个管理。对,管理他也有自己的,比如他记忆。 - -王名正 01:16:30 -他也有自己的虚拟币。 - -卡若 01:16:32 -记忆的功能,对吧? - -王名正 01:16:32 -业务功能每个管理组管理内容不一样,是一个可以吸收给这个随便举个例子。 - -许永平 01:16:32 -也包括我们,是吧? - -卡若 01:16:34 -他每一个管理就把他的能力往下去拆分了,对吧? - -许永平 01:16:34 -他每一个管理者。 - -卡若 01:16:39 -他管理有管理的能力,然后他底下把能力分配给他底下的团队成员去执行嘛。 - -许永平 01:16:39 -他管理。 - -卡若 01:16:46 -每个人是不一样的,性格也不一样的。那比如我们要做一个分类的话,他自己会去吸收一些东西出来,吸收给这个团队,知道吗?我随便举一个例子,你看像。 - -王名正 01:17:02 -像这个。 - -卡若 01:17:02 -这个我好几个。 - -卡若 01:17:17 -你看他比如这个手机自动操作的,是吧? - -王名正 01:17:18 -任何问题都可以问。 - -卡若 01:17:24 -这个我已经有一个版本的能力,你看我这一个。对吧?然后像这种搞流量的自动去做排名的,对吧? - -许永平 01:17:39 -好。 - -卡若 01:17:41 -把这两个这个文档的这个核心代码跟它的一些那个流量获取的一个形式,形成自动化操作的一个stream。然后你安排,看安排谁来安排这个能力到哪一个人设的身上?然后哪一个人的身上?你把这个放到卡若 AI 底下。的一个一个skill,然后用中文名来命名嘛?我跟你讲讲一点是怎么样去吸收一些你能看得懂的一些能力嘛。这个首先是自己能看得懂并且执行过,好吧? - -许永平 01:18:12 -这两天一起实现你这个通过百度的算法,但是他新做的。 - -卡若 01:18:16 -因为这个是通过百度的那个巨峰算法去做 SEO 排名的,那你当他吸收这个能力就可以了,他自己。过一下这个过程就知道了。 - -卡若 01:18:44 -连志刚刚发的吗?发啥?我刚发飞书,发啥?你看。你说这个机器人。两个项目核心能力的 schema 并归属 to b,它就属于这里,我就很它就分配给一个人去了,它。 - -许永平 01:18:59 -他就分配给一个人去。 - -卡若 01:19:02 -自己知道谁来做这个事情最合适,看到没有? - -许永平 01:19:02 -他自己知道谁来做这个事情。 - -卡若 01:19:08 -在这。嗯,那你就知道他为什么分配给他,我都不管流量自动化,看到没有?你看看这个人干嘛?他自己去招了个人来干这个事,知道吗?那你这个人是通过他的性格来定义的,知道吗?你看引流滋养它记忆联想性格,这爱表现的是吧? - -许永平 01:19:45 -对。 - -卡若 01:19:49 -你看手机和网页自动操作,你看这个是我之前做那个网站排名的和那个淘宝排名的,十几年前的东西了。 - -许永平 01:20:01 -他们还有另外做法,就是定义一种名正。 - -卡若 01:20:03 -你看。还有另外一种做法,就是定义一种名人的。名人那个很容易,你把他名人的能力吸收掉就行了。 - -许永平 01:20:08 -但那个对,就是比如说什么雷军,对啊,就是你还得去提炼他的过往,可能他写的一些书什么的,让他去清洗,提炼出他的能力,其实就是定义他的能力。 - -卡若 01:20:12 -就是比如说什么雷军这种乔布斯啊。那个不完善,你首先得有他们的库。对啊,你还得去体验他的库嘛。那他写的一些书,或者就是你们的能力,把他定义完之后,他知道你的能力边界他自己会去吸收。就是定义他的。知道,反正我。我给你们分享一下这个的一些新的一些用法,它就这里边它就是 5 个人去招募了,现在有 17 个人, 17 个人每个人解决一个问题,是吧? - -许永平 01:20:35 -一些新的。这里面招募了 17 个人,一个人、两个人。 - -卡若 01:20:45 -然后我能做什么?你看自动化操作网页刷流量,什么什么什么什么,因为这个我已经实现了,我根本就不用去管它,因为我知道它实现逻辑跟问题,然后你看我还可以干嘛呢?用那个卡若 AI 的整个的团队来分析一下这个skill。帮我提一些优化的建议,然后最后形成一个PK,看谁的建议最好。你看整个团队去优化它。他这个层级应该最底下是 CEO 人力,然后在上面一层就是工作流,在上面一层就是属于这种数字员工。就是这个就会变成什么,你知道吗? - -许永平 01:21:28 -这种场景。 - -卡若 01:21:30 -就是他。他是把你的能力,这么多年沉淀的能力做拆解的,并且形成一个记忆的一个形式,然后分配给执行的人,而且不会出错,你能力拆得越细就越不会出错,知道吧。 - -许永平 01:21:32 -然后。 - -卡若 01:21:50 -所以怎样去把这一些,包括那个视频剪辑切片的一些能力,你多方的去验证它有商业知道有开发、有商业、有流程,对吧?有。有一些 7 的, 8 的,你用久了之后,这一块的话,它是就会给你一个很完善的一个东西,就等于十几个专家,那这个专家你觉得只要你一定要验证过,不要让他吸收垃圾就晕了,就会变成。 - -许永平 01:22:12 -是。好的。 - -卡若 01:22:21 -越来越垃圾。 - -许永平 01:22:21 -也有遇到。 - -卡若 01:22:22 -也不会变成越来越垃圾。他会性格会变成什么?变成 enfj 啥都学,你这性格会变的,你会发现谁变成 enfj 了,然后他的。这个岗位又不错了,那肯定这个吸收了很多垃圾了,知道吧? - -许永平 01:22:32 -到了这个岗位。 - -卡若 01:22:36 -为什么定义性格知道吗?它会变的。那你看吧,它这一些我就给你们过一下它是怎么样去实现的讨论的,包括你那个这些智能提问什么的,它都自己帮你跑完了。知道,那我等一下给你们讲本地模型怎么用嘛。不是跑本地的吗? - -许永平 01:23:01 -这是靠本地包。 - -卡若 01:23:03 -他一部分跑本地的,所以我消耗很低的。你别以为这个消耗很多,我消耗很低的,我等一下给你们看。为什么?你看他就研讨会,你看他参与着 ABCDE 会议开场大总管开始,他就开始汇报。 - -许永平 01:23:19 -可以。 - -卡若 01:23:27 -之前我也想过,我等拉个讨论群,先把目标丢进去AI。大家先自己去讨论一遍每个人。你要你这个要把你的团队变厉害,知道吧?你看他就,嗯,你看他提的很多的建议,是不是每一个人有每一个人的观点?模拟现实我们开会的那种场景。 - -许永平 01:23:50 -模拟现实我们开会的那种场景。 - -卡若 01:23:52 -是不是?而且他很快,那看是不是?多给执行。 - -许永平 01:24:03 -用本地跑的吗? - -卡若 01:24:03 -本地跑的吗?这个。来,我给你们看一下本地的这一些东,我就给你们多分享一下这些东西,大家。刚才不是调 c 了吗? - -许永平 01:24:09 -刚才不是掉 c 了吗? - -卡若 01:24:10 -用cursor。 - -许永平 01:24:10 -不是用 cursor 吗? - -卡若 01:24:11 -我用cursor,我当然有更好的解决方案,我给你。 - -许永平 01:24:15 -这是用 cursor 跑出来的呀。 - -卡若 01:24:15 -用 CURSOR 跑出来的呀。才不是呢,一部分 cursor 而已,给你看这个,这个应该是本地的模型,我看一下,不在这,我其实都不关注它在哪里。 - -许永平 01:24:18 -cursor 你看。 - -卡若 01:24:33 -不需要关注它在哪里?我看一下这个放在哪里。有一个盆,应该是在那个这里。 - -卡若 01:24:54 -看一下小程序飞书管理。 - -卡若 01:25:05 -那个本地模型的在哪?来我,你打开。那边有搜索吗?第2个有。 - -许永平 01:25:12 -要做。 - -卡若 01:25:14 -本地。 - -卡若 01:25:24 -卡火。这在卡火里面看一下。因为我现在就不关注的这个跟团队成长是一样的,当你到团队到 20 个人的时候,基本没空去看,只要记住他名字跟能力和他的那个循环就可以了,你知道吗?这个是啊,在这还没有本地模型,给你们看一下你就知道咋用了。你看这个本地模型管理,我本地有三个模型的,但是它我不会,是,它自己会去启动。是提醒当卡若 AI 使用本地模型时自动使用提醒,是吧?它有三个,一个叫轻量,我定义了两个东西,这个是千问的。轻量是来解决文本摘要的问题的,它只处理,而且。 CP 会控制在30%,它不会控制太多,所以它不会,我怎么用都不会卡。那这个提醒,这个共享模块是任何的 skill 都可以调用的,等一下给你们看共享模块嘛。 - -许永平 01:26:45 -这共享的模块是一样的,都可以。 - -卡若 01:26:54 -那这些就是一些基础的一些参数,在 cursor 上,你看。现在是用了这个隧道,不然你启动不了。 CURSOR 是可以直接调的,就是他需要做文本向量化跟简单的文本的总结的时候,他就会调本地模型,他不会调那个 cursor 的东西的,大概给你们过一下,所以我你看这三个是。是都有用途的轻量对话,一个中等对话,还有一个文本向量化,三个不一样的模型,干三个不一样的事情,他自己会去使用和调用这个东西,这他需要做简单的动作的时候,他就会去找这个事情,那这些就调用的形式就不看了,后面这些是调用的形式嘛。然后它跟你看它的性能对比,这里面有个轻量的,这两个是千问的差,有差多少你自己看一下,就差它只有 20% 的那个 Claude 的 4.5 的 20 的能力,那它只有它 35% 的能力, sonnet 是那个 Opus 的那个85%,你就做能力对比,它自己会去猜嘛? - -许永平 01:28:10 -他有他的。 - -卡若 01:28:32 -然后这个是免费的嘛。就这一个,你看这个差 5 倍, Sonnet 跟 Claude 的价格是差 5 倍的,跟 o 那个 Obeus 两个,是吧?那肯定本地的不上传云端,它有一些好的一个地方,它读取文档秒级的咔一下就读完了,是吧?根本就不用去搞来搞去很麻烦,所以有一些向量化的东西,为什么我那个读。 - -许永平 01:28:56 -用例,对,好。 - -卡若 01:29:02 -他们读很多东西会很快就这样嘛?那他自己这个 schema 他读完之后他就自己会去做了,应该简单代码就跑到这里了,就是简单问答,就他们来高质量需求的时候,就他来复杂代码他来,那他会根据你的链路去拆解嘛? - -许永平 01:29:15 -来。好的。完了。我。好的。 - -卡若 01:29:24 -你看这个基本就是只能简单问答就优先他,因为都差不多就不会用这个了,对吧? - -许永平 01:29:25 -不客气。 - -卡若 01:29:33 -然后一些多轮的,就你复杂推理,当你说了很多东西,他拆了很多的任务的时候,他就会用,他这是他们自己那个,那像这个批量处理的,就本地跑的,他就用最轻量的,这个都不用钱,这个就是跑跑本地机器,那离线也能用啊。所以我为什么有些时候坐在车上我就自己跑这个了,对吧? - -许永平 01:29:55 -所以我为什么这希望? - -卡若 01:30:00 -去做一些总结文档跟文档。同理的事情,你车上就搞定了,不用管它上不上网的问题。对。 - -许永平 01:30:07 -是。 - -卡若 01:30:07 -对吧?它离线也可以跑,这个就离线了,而且这个我不需要,我就我不需要切换了,我就是正常用 cloud 就可以了,它自己会去调嘛。响应时间也很快,然后它怎么这个是等。等于他有了这个调用的能力。好,那这一步的话是融合各个的 skill 怎么样去调,对吧?使用场景a、b、c、d,就是他告诉他,你可以这几个人告诉他们什么场景,比如会议摘要,他就调的是这个,所以刚刚看的那个会议摘要为什么丑呢?你没定义清楚,他就丑,因为他用的是基础的小模型在跑,知道那文字。清洗也是他可能用的,你如果你定义成他用那个归零的那个就很漂亮,对吧? - -许永平 01:31:08 -对。 - -卡若 01:31:10 -不然他就给你做那个总结文档卡一下子,是吧?那需求拆解这一些也可以,一些小的拆解需求也可以的总结拆解,是吧?他就会做这一些事情,那这个是他怎么调?你就告他自己会告诉那个 AI 怎么样去调嘛?那这个就是两个,那这个是他这个是吸收了别人用这个是最佳的一个实现的一个形式,长文生成跟一些别人用小模型跑出来的一些策略,你把它吸收掉就好了,所以他已经吸收了一些能力,包括这一些,然后核心的一点,是吧?你看结构化输出等等。 - -卡若 01:32:02 -所以有一些东西他也能做得很好,就是别人已经把做得很好的那些方式已经做好了,所以我也有很多,包括读书笔记 7 的、 8 的这种,他其实对我来说不用钱的,我跑很多是不用钱的。对吧?你这样包括这个向量检索速度非常快,他本地跑比你慢,慢传,再让他服务器解析,再给你回过来,这差很多的。对吧?像这种一些内容,然后异步这个处理多个请求,因为你包括那个cursor,它也是流水式的,很慢的,它这个可以同时 10 个线程做 10 个不一样的事情,做完任务拆解成 10 个人同时在做,它速效率就很高了,它这个等于它是一个团队在。 - -许永平 01:32:35 -对。 - -许永平 01:32:45 -那。 - -卡若 01:33:02 -在做一件事情,你在,我们在 cursor 上面用它其实还是一个人,虽然多线程,它多线程处理的是不同任务,它这个的话是一个人先把任务拆解完之后一个人把一个团队把这些拆解完的任务全部完成,它有这个好处,就你用了才知道。 - -许永平 01:33:21 -这个呢。 - -卡若 01:33:23 -像这种,你看它怎么用融合点,怎么去用这些steer,它其实都有调用的,那这个是一些更新的嘛?你看对。这个是他会更新汇报变更,你看添加流式响应工具,调用 1 月 28 号的,你看。 - -许永平 01:33:34 -对不起啊。 - -卡若 01:33:43 -就一些融合的一个形式,你看这个 cursor 就集成了通过这个隧道的技术就集成了 cursor 就能用,不然你直接本地 cursor 是不给你改的,改不进去的,但是他能直接那个嘛。 - -许永平 01:33:55 -person。person。我给你改。 - -卡若 01:34:05 -然后你看他就把这个融合到现在有 34 个skeleton,对吧? - -许永平 01:34:09 -lesson, we are 在这里的。 - -卡若 01:34:11 -那我以后的以后我已经让他做了一个方式了,就是像以后生成 skeleton 的时候,他都会调用这个,这里又有个共享库,是吧?这个是共享模块,他是调用模型的共享模块刚刚只是模型。真的这个的定义,那这个是属于共享模块,是吧?你看谁在管理?你看这个是谁在做这个事情?所以他会找这个人要这个能力,我要调模型,他逻辑是这样的,我要调这个模型,然后开始去找ABCD,你在做什么?什么什么事情,然后开始把这个做什么什么事情的相似。 - -卡若 01:35:02 -东西快速地去找相应的模型,然后对应进去,然后更新,对吧?这个是他共享的库,去就会调用这个本地模型,就你在对话的,用这个 AI 在实现对话的过程当中,什么时候调什么来解决这个问题,对吧?这一个是一些那个使用的一些方式,那经验库就不说了,你们之前做过。现在做的整组的日志汇报,你看他做了什么,每天都会记录的,这个是记忆的功能,知道这个等于一个小公司,你知道,你看他做了啥嘛? - -许永平 01:35:44 -是。对。 - -许永平 01:35:52 -那谁对他今天。 - -卡若 01:35:53 -霍翠, 1 月 29 号,对吧?内容代码修复、迁移,他今天就做了一个事情。没了锤炼代码,他是负责整个一个目录的那个技能的迁移,他来负责这个事情嘛。那至于这个他的一个能力就是放在这里面的能力,就是 schema 这个只是他的日志而已,就相当于他的工作日志,你看这个也是一个技能目录迁移,因为今天他们只做了迁移的事情,这个今天才建完。所以他在执行每一个步骤操作的时候,这个是属于记忆功能。那像这种的话,它只是一个组织架构,是吧?这属于日志记系统,哪一个人执行的,他就自己去定义,然后汇报的格式,这个跟咱们现在做的事情是一样的,只是。 - -许永平 01:36:53 -这个东西。 - -卡若 01:37:02 -把他的自己的能力拆细了,你看一些自动触发,当我执行这个操作,他就会自动地记录到这个日志里面。由谁来记?自动记录这个日志,那像我们在做,你看视频切片,就你看这个木叶火影忍者切片的叉叉视频,他自己会去记录,那我们在看他整个的规划的当中,包括我们自己在做。那个记忆的过程当中就会比较清晰,什么东西做了什么嘛?这是他们自己记录的一些事情,而且他这个文档相对会简洁很多,好吧? - -许永平 01:37:42 -周三自己这些,他这种。 - -卡若 01:37:50 -这个是一个日志,就是像这种就搭建的时候,你市面上你找不到这种东西,这个核心的一点,你就是把自己的能力抽象以及。 - -许永平 01:37:51 -这个是他是搭建的时候,你市面上找不到这个核心的一点,我想。 - -卡若 01:38:02 -那个让 AI 自动地去帮你去多探讨嘛,多聊多探讨嘛,是吧? - -许永平 01:38:03 -那个。他可以看。 - -卡若 01:38:09 -那你看它这个就执行的一些东西,应该把这个 PK 的结论,你看优先级最高的优化,合理合规的潜质跟选择表,等它一整个团队的能力去帮助了这一个新出生的skill,完善了它的新的一个 skill 差吧。那这个 seal 出来就很强了,首先是一整个团队吸收了整个市场最优的东西,然后他们来优化,这个时候你再把他们让他去帮你做一些事情,就非常厉害。另外是不是流量自动化?诶,现在那个,嗯,高数 GL 那个本地能跑吗? - -许永平 01:38:55 -诶,你现在那个 autoglm 那个本地能跑,还是这个是云端的? - -卡若 01:39:01 -还是这个?是有一个。本地当然。能跑,它,就是它这个就是用 ADB 去控制手机。 - -许永平 01:39:04 -现在有了,那这个不是说安装很麻烦吗? - -卡若 01:39:09 -这个不是说安装很麻烦吗?包方法现在可支持了。 - -许永平 01:39:13 -现在可支持了。 - -卡若 01:39:14 -不麻烦,不麻烦,它就是个命令行而已,就像我经常控制里面那个大屏的那个,就用这,也不用这个,用 ADB 就可以了,它这个只是封装的,就封装了很多的功能在里面,没啥神奇的,知道吗?不神奇,然后你看它这个就是。可以实现,我以前的那个 PC 百度、淘宝去刷量,它是按时间节点阶段,然后按照百度的一个飓风算法的算,去匹配它的飓风算法,并且实时更新它飓风算法去匹配按时间节点阶段,然后安排它引百度的蜘蛛去,所以我当时为什么排名那么好做?其实背后有2万个机器人在跑。对吧?所以很多东西就能做了,那之前用去check,你看那去。 - -许永平 01:40:01 -下。 - -卡若 01:40:02 -APP 和 ie Windows 自动化操作嘛。 - -许永平 01:40:04 -我上次有下那个autogenml,然后但是跑不起来。 - -卡若 01:40:04 -上次有下那个 auto js 的,然后它是跑不起来。你把先把它抽象成自己的就能跑,你不要用它的跑,很麻烦。你把它吸收成自己的东西,你就自己知道它逻辑了。当当这一个团队都是从你抽象出来的时候,当他们写出来的东西你是很清楚的。所以不用去往上,不用去摘抄他们的,如果你不理解的情况下。 - -许永平 01:40:31 -我是把那个。模型下起来,跑不起来。 - -卡若 01:40:33 -对,你如果不理解它逻辑,摘下来是没有用的,我从来不会去看任何一个别人写的这个东西,我就拿过来拆解一下,直接让我就团队的人相当于这个就是一个团队去吸收一下,我就立即知道了。 - -许永平 01:40:44 -对。 - -卡若 01:40:48 -就一个人解释不清楚,那你们十七个人自己去 PK 一下,哪一个说的清楚?是不是它慢慢就会形成这个一个东西了?你看这个就是之前设备。重置 VPN 切换,是吧?刷量,对吧? - -许永平 01:41:06 -对。 - -卡若 01:41:08 -那这个它基本这个能力就很清晰了,你看安卓多模态,你想一下这个,这个这一个东西 12 年做的,现在还能用的,你这个很早就有了,知道吗?让百度、淘宝刷量, ie 自动化之前就自动控制浏览器去模拟真人去。访问。自动去切换那个电脑的那个虚拟机。 - -许永平 01:41:35 -接完了电脑。 - -卡若 01:41:39 -自动去访问、自动付款刷单。 - -许永平 01:41:39 -知道了。 - -卡若 01:41:44 -那它这个我就主要是这个逻辑,给你们讲一下这个逻辑,其它这些东西,它不是什么很厉害的东西,就是经验而已。是吧?但这个就那个ADB,就是控制这个手机端的,我们现在用的这一个也是用。用那个 ADB 封装的那个那SDK,你看它,其实你看做网页刷自动化,它这个因为我之前是用 Windows 的,它自己会去翻译成 Mac 的手机的事情,所以我根本就不担心它。 - -许永平 01:42:23 -外。 - -卡若 01:42:27 -不是,我只是随便拿一个能力去抽象一下,那这一个更好。那屏幕感知,那我现在不是装了虚拟机的,是吧?然后它的一些代码交互形式,这一看你就很知道,你看打开什么,什么什么。 - -卡若 01:42:54 -他就是刷一些流量嘛。刷流量就是一直访问这样。 - -许永平 01:42:58 -刷流量就是一直访问这样。 - -卡若 01:43:00 -模拟真人去访问,里面有。 - -许永平 01:43:00 -模拟真人去访问。 - -卡若 01:43:02 -很多算法在里面,但我已经实现过,我就不关注算法了,你看嘛,这百度APP,你看它自己打开,自己浏览,以前是模拟按键去访问,然后把时间定好,大概多久多少范围内一个个去解决。 - -许永平 01:43:03 -算法,不管是算法啊。看这百度,看他自己模拟按键去,然后他自己。 - -卡若 01:43:20 -现在让 AI 自己搞定就行了,你不用那么麻烦。那你看百度搜索刷量嘛,你看以前多简单,关键字加翻页,点击流量自动化,那里面。边有一些算法我是知道的,你是这个不一样吗? - -许永平 01:43:33 -他。 - -卡若 01:43:38 -商品浏览要停留多久? - -许永平 01:43:39 -对。 - -卡若 01:43:39 -要看看别人的,再看看自己,对吧?这些都是一些细节,一些事情,那这个就要伪装嘛?你的设备就不断地去那个reset,那个 CPU 跟 reset VPN,对吧?那现在也一样,你可以虚拟个 Mac 或者那个Windows,也可以,它通用。它是等于通用了自己长出来的,就像也做这个调一下,可能三天、两天就调完了,它就能跑了嘛。那以前写这个得 8 个月一年,对吧?多多麻烦,现在两天搞定了。那你看它这个就有一些命令参考是给 AI 看的,不是给我看的,因为我知道已经实现了。知道吧。那你看负责流量自动化招商运营口头禅流量来了,你看。我到时候我要提一个东西,我要做这个网站的 SEO 或者Geo,对吧? - -许永平 01:44:34 -DOC 对。 - -卡若 01:44:39 -他是不是自己会帮我去执行这方面东西? - -许永平 01:44:40 -他是不是自己在帮我们? - -卡若 01:44:42 -我就在 doc 上面铺两个虚拟机,他自己去给我建。自己去刷流量,自己给我灌流量,我还搞什么 Geo 的网站,麻烦,是不是?我就再没有,我就视频上什么 Geo 的源码,搞几个让他吸收再执行一下,是吧?就是现在很多为什么开源,你不开源没有用,更多的是我们把这些能力变成现实的生产力,能产出的生产力这才有用嘛。 - -许永平 01:45:03 -就是现在很多为什么开也不开,也没有用,更多的是我们把这些能力变成现实的生产力,产出的生产力,这这这个整个就这样。 - -卡若 01:45:15 -那这个你看它就整个就这样嘛,对吧?那就完成了嘛,是吧? - -许永平 01:45:20 -完成了,那我这个我肯定让他,我当时搞天猫店, 1, 500 万个。 - -卡若 01:45:22 -那我这个我肯定知道他执行是什么样的,因为已经执行很多年了,之前我当时搞天猫店,一天5万个访客,就这么搞来的,知道其实大的。到质检知道就2万客,虚拟的人天天给你看这淘宝,天天都觉得我的是最火的。 - -许永平 01:45:32 -大道至简,2万块钱的,推荐你看淘宝,天天都觉得我的是最。 - -卡若 01:45:40 -对。 - -许永平 01:45:40 -是。 - -卡若 01:45:41 -知道, 13 年到 16 年特别好用,到 16 年底特别好用,后面就增加刷单就行了,就需要有人,你看像这个是整个的一个搭建的一个逻辑,我其实就做了两步,一个吸收,一个让现有的团队去评估并且去完善它,对吧? - -许永平 01:45:42 -30 ~ 16 年特别好用。 - -许永平 01:45:55 -及时复盘。谢谢。 - -卡若 01:46:03 -那他就有很多的记更改嘛?谁谁谁,你看谁建得最好,你看他 PK 了,我把这个隐藏,你就看他的结果就行了嘛。刚刚那个是过程,那个一般你熟悉是不用看的,点不了不重要,你看关注点环境风控结构清晰好找,能跑好抄,边界清可以。扩展合规,他把你几个的点都给你解决掉了,就是你关注的一些点。对吧? - -卡若 01:46:50 -那你看谁建立的最好吗?合规红线,对吧?他会防执行时。失败,各种他帮你考虑的很周到,就各个能考虑到的角度都会帮你考虑到,懂吗?他给你建了一个一键检查的示例指令,怕他们自己读不懂,对吧?这个就是有确定一些东西长期维护就是他,对吧?自己会去PK,然后就那个嘛。你看增加何时用SKIA,何时用流量自动化表格,对吧? - -许永平 01:47:43 -价。 - -卡若 01:47:46 -他这个给你的一整个结论就这样,环境清单嘛?这个我们怎么像远志碰到这个问题,就是有个环境检查清单,他你是 Windows 还是Mac?我提前检查了一下,优化完之后他会就会先看这个 Windows 什么的,他该装什么? - -许永平 01:48:00 -他会做一些。 - -卡若 01:48:02 -然后给你一个优化建议,然后你选就行,就不会出现跑一天的问题,就这个行动项继续迭代,对吧? - -许永平 01:48:06 -对。 - -卡若 01:48:16 -一键检查就把上面的大家考虑到的东西检查,然后他后面的话他就积累经验了,就是你做的一定量的时候就积累经验,积累经验就让他每天吸收就行,你有做。 - -许永平 01:48:24 -一起工作。 - -卡若 01:48:32 -做越多,他吸收越多,基本就不会犯错了。 - -许永平 01:48:32 -合作我觉得大家吸收也。 - -卡若 01:48:36 -那你尽量经验库不断的去积累嘛,我这他已经有转化的记忆机制,你看有一些东西他就需要这个事情,然后他们开会研讨的,开会的越开就越屌,开越多越屌,是吧?所以他就自己会去增长。只是我现在没加一个事情,是什么?就是。是那个自动跟他聊天之后让他自动去工作的自动化定时登录,相当于,对吧? - -许永平 01:49:04 -那个自动那个。 - -卡若 01:49:13 -就是那个我微信给他发条指令,这一些人干什么事情我是清楚的,你用一些让他像豆包手机的这种,他一直循环是因为他的能力都没有验证过,就是给你一个通版,然后让他理解这个行。行为规范,你给他什么东西,他们能做什么事情都不知道的,是吧? - -许永平 01:49:36 -大游戏。 - -卡若 01:49:37 -他们要细化,那这个本质上是拆解能力细化,并且这些能力是之前你实现过的,那他就你就会越越用越越顺,越用越顺。知道,就像老王这个,你看分销的、分账的,对吧?能力是不是快速让他吸收一下?是不是就像我这个,这一个现在这么多的切片的团队,他们去做分销的?的过程当中,我们这个分销方案是如果能快速的裂变,让别人愿意去分发,每天去坚持分发,这个也是很好的分销方案了,我就直接把它吸收过去,它应该就会分配给卡火的某一个人身上。 - -许永平 01:50:16 -对的,那以后我要做的是。 - -卡若 01:50:18 -那以后我要做分销的时候,我就加上分销的功能,它是不是按照这个那个一级绑定 30 天的方式去给我生产出分销的功能出来?我根本就不用想,因为。 - -许永平 01:50:31 -我们。 - -卡若 01:50:32 -我做过,对吧?这个是很重要的一一个一个点,就咱们在做这些的过程当中,其实现在 AI 就是吸收,就两个字,就是吸收,你把它当成一个人一个团队,由一个人拆解成一个一个团队,反正我都有录屏,你们可以照这个去说嘛?组建成团队去说,他无非一个主管,底下团队成员的性格匹配主管的性格。他去招人,是他按照他去生产一个人,不是招人生产一个人,是按照他的性格喜好来和配合度来招的,知道吗?你看这个跟老王是一个性格的,知道吗?他去找的这几个什么金仓、金盾、金建,什么金链,这。 - -许永平 01:51:29 -来。 - -卡若 01:51:32 -这几个人的性格是跟他搭配起来舒服的,你知道。你这些人除了说 cursor 自己生成的。肯定不是 cursor 自己生成,是因为我很知道这些 MBTI 的这些我学了好久的。知道,所以他找什么样的人配合舒服我是知道的,所以他在做的时候,过程当中会出现一个问题,你吸收的技能很多、很杂、很乱这一个。 - -许永平 01:51:50 -知道,所以他的账号。 - -卡若 01:52:03 -卡兹就可以把这四个人做一个技能的调整和分配,他知道谁来做这个事情更合适。你还整理啥目录啊?他帮你都整理好了,他以人为单位去帮你整理好了,是不存在这一些事情的,所以他这个是属于自己的一个 AI 的一些搭建,你看他现在就有很多能力,我用大模型我根本就消耗,我买一个我可以用半个月,我用那OP。 - -许永平 01:52:29 -呃。这个东西。 - -卡若 01:52:32 -EA 是最高级的,我至少用 10 天,每天问,知道他不会对我来说消耗只有原来 1/ 10。知道,所以我可以很是那个随便去用。那我不用高级的版本,我用这个他也一样,他该消耗不消耗他就调本机的模型,本机的模型我也不用担心了,他就最多用我 30% 的性能,对吧?他不会超过 30% 的性能。有个千问后边。 - -许永平 01:53:02 -有的,签过的。 - -卡若 01:53:04 -清华问题能力还可以。千问还行,是吧?所以它这个是一个降本增效的一个体系搭建,那回过头来我们现在做的很多的东西,它这个只是提效的原因是什么?像远志这个我说一个具体的一个一个事情,你视频剪辑出来是不是要快速地复制,对吧?我们就剪一些视频,把之前几年的什么存客宝视频全部丢出去。全部剪,让它自动发,是不是一个 AI 的库就出来了? - -许永平 01:53:35 -有。 - -卡若 01:53:37 -你只要做视频的筛选和整理,然后一键分发到抖音上面去引流,获客就出来了,是吧? - -许永平 01:53:46 -是。 - -卡若 01:53:47 -所以这一个就你就核心的目标就是它能第一个能跑嘛?能跑,跑完之后的话就是它有一个多久的时间嘛?你现在 5 分钟,是不是第一个闭环跑出来? 5 分钟剪一个。一个两小时的视频,是不是?那你就相当于一台机器 60 分钟,你把它满功率 60 分钟,是不是 12 乘以两个小时, 24 小时,一天的视频一小时就可以剪 24 小时的视频,一台主机是可以量化掉的。对吧?那你 24 小时的视频,你一个视频平均 3 分钟, 24 小一小时的视频差不多有两个主题,那你 24 小时是不是剪 48 个视频? - -许永平 01:54:24 -这。 - -卡若 01:54:32 -视频一小时剪 48 个视频是不是能算得出来?那你一天工作 24 个小时,算 50 个,算 20 个小时,一天 1, 000 个视频剪辑一台机器,那是什么概念,对吧? - -许永平 01:54:42 -一块。 - -卡若 01:54:46 -你 1, 000 个,那海宁集团剪 1, 000 个视频得一百二十几个人了,是不是一个人就干完了?那后边就解决素材的问题,素材就是你看我,我为什么去丢链接这个给你们看这个丢链。链接的这一个过程是干嘛呢? - -许永平 01:55:04 -干嘛就直接告诉他这个链路? - -卡若 01:55:05 -我们有录了很多视频,你就直接告诉他,你去,你这个链路你还没跑嘛? - -许永平 01:55:12 -你去把。 - -卡若 01:55:12 -你去把飞书上这几年所有的关于产研的几个关键字,什么什么的视频全部给我下载下来,自动地去剪,你需要一个个去下吗?但这个跑起来会有很多卡点的,所以我没有先去说,先让你跑一个最简洁的,不然你东卡一下,西卡一下,就没信心了。知道会有很多卡点的。知道吗? - -许永平 01:55:35 -知道吧。 - -卡若 01:55:35 -对。那你这些会议是不是快速整理出来,大家就有很多一些分享的一些东西,而碰到一些问题,你可能就剪完之后就是 50 个视频里面挑个 30 个, 20 个是吧?里面去筛选一下, 30 个, 20 个聊一些核心话题的,跟我们进度相关的内容的东西。对吧?那你再到时候做那些什么会议纪要,也好,做那个视频和解说方面。 - -许永平 01:56:01 -要试。 - -卡若 01:56:02 -的东西就快很多了。那么所以这个是那个工作方法的一些点,是吧?那你剪这个剪完之后,为什么我还那个?今天还说那个剪完之后是不是你搞一个专门的一个飞书群或者微信群,你剪完的视频是不是自动的丢到这个群里面去啊?这些都是自动化的一些事情,因为还这个你要商用就也很快。 - -许永平 01:56:29 -这个要上 6 五点。 - -卡若 01:56:33 -是吧? - -许永平 01:56:33 -好吧。 - -卡若 01:56:34 -去做商用的一些事情也会比较快,那我就大概给你们讲一下这个这一些逻辑,然后再稍微再补一个,那个付款那个还没搞定。 - -许永平 01:56:45 -再找一个。 - -卡若 01:56:49 -付款扫码的那个,你们这个得排期排出去,排得排过去,因为这个我给你看一下,就这些远志到。 - -许永平 01:56:51 -对。 - -卡若 01:57:02 -说要排一下时间,就是我们这个也有一个阶段性的,那个我这两天不是去吃烧烤嘛?那你给你们看一下有一个场景,就是我,我们现在没有那个付款场景,所以这一个这些这种客户就拿不到了,知道吧? - -许永平 01:57:21 -不要投。 - -卡若 01:57:36 -看一下,这个是付款场景,用人看这个是我拍的一个视频,才 67 的播放量。 - -许永平 01:57:37 -不还行。 - -许永平 01:57:45 -不方便。 - -卡若 01:57:48 -嗯,我看一下有没有声音,都切本机。 - -卡若 01:58:00 -我这边。 - -许永平 01:58:01 -这边来看一下,是。有声。 - -卡若 01:58:05 -那没关系诶,等一下,我应该点一下。不重要。今天和我老婆在金连龙这边附近新开的这一家店,过来吃,这个味道非常。反正就是烧烤。经常员工聚会来这里,然后这里的话刚好就在抓到的那个老板,我觉得他们从公司楼下生意现在会这样,然后感受感触一下。不利于咱们做的是电商 8 家门店,今天上海有 8 家门店。我就跟他讲做私域的事情,但是这个要付款码,要门店的,要付款码行业这个数据交互,付款码获客就会大大提升的。是吧?然后这个的话,你看我写这个视频,这金玲珑的老板就过来了,早上发了一堆东西,加了我微信,你看这。 - -卡若 01:59:02 -金龙的老板,一个小女生,东北的,但我得拿得出手东西呀。那你看她在广州上实习课,然后早上一点多还睡不着,给我微信发了一堆消息,我五点多给她回,五点多还在,还没睡觉。知道,等她来厦门就约她,那她就需要我们这一个解决方案,因为她现在开店。是很快的,他短短半年开了 8 家了,因这家是的确是做得很好。是吧?但这种,但我们现在付款的这一个我没办法给他演示,你懂吗?是不是?我说是这么说,我就照着他的那个,你看刘思雨,我照着他的桌子上面,我说你可以用这个扫码付款,能加到微信,是不是?这属于连锁门店的一些合作的一些东西嘛。 - -卡若 02:00:08 -所以为什么我们在那个各个的一些层面,包括那些东西上面嘛?是吧?其他的不说太多了,是吧?这个就是我们那个,因为我现在不知道一点是什么扫码的进度,我不知道啥时候用,我要跟他聊,你看他明天后天就过来跟来公司,我。拿给他看,我没有东西给他看,我只能给他看视频,所以我能扫码扫一下他的小程序,跟我们对接一下。 - -卡若 02:00:40 -那这个就很好的一个东西,知道他开门店,他要不要缺钱?找金融公司要不要贷款做数据登记?要不要去做数据登记?补贴 10 万块,我们一人一半。对吧?有很多的事情可以做的,他私域也可以给我们做啊。扫码付款给团队跟上。对。先进我们这,再进,转到他公用的客户头。就是我们先有个实现的一个扫码付款加微信。就行了,是不是?那这个得你至少有要准备的一些东西,但这个是另外一回事的。快速地让每一个付款码小程序,然后把生成绑定了自己的APP,然后去加。那就放到存客宝那个上面也可以。对呀。对吧?就有这么一个功能,至少能给他看,能放到那边给他看吧。测试的。很多人过来,包括陈国过来演示,他也演示不了,是不是他每天那么多要合作的,对吧?我们一家那边放两个手机,对吧?一家那两个手机弄一个,或者是不是自己家自己动,这个体验感是不一样的。然后中台一个电脑屏幕放那边也可以。 - -卡若 02:02:02 -可以,人家一看诶,或者放我们那边都可以,是不是他就有场景化的东西?因为我每天输出太多,我是怕你们会乱,知道吗?所以这个就为什么要去搞搞这些视频跟整理,不然很容易就很容易乱的,是吧?但这个 AI 的确是很高效的哦。对。那就明天那个出的永平再过一下吧。好,我明天肯定是要让他们用的,不然他们每天我都不好说了。好。是吧?没事,就这样吧,那衍子那个剪出来视频就丢群里,很丑。效果不好。你就跟他讲要加字幕,多加一个,因为他有两个版本。 1 个不加字幕,一个加字幕。因为我做的 skill 我就比较知道,所以为什么我只能提供思路给你们,你们要自己做,自己说才说。 - -许永平 02:03:13 -差点忘了。想问你那个,你是哪?你是用哪个去生成那个微信小程序的?我看到目前市场上有三个框架,你那边是用哪个? - -卡若 02:03:23 -我没有任何的东西,我就是告诉他把 H5 变成微信小程序,没了一句话。 - -许永平 02:03:23 -我没有的,我就是告诉他把那些。 - -卡若 02:03:28 -上次你跑的时候,他底下是有一个命令的。 - -许永平 02:03:29 -上次你跑的时候,他底下是有一个命令的,我不。不知道你是用。 - -卡若 02:03:32 -没有命令我就告诉他你用 H5 的这个前端给我变成微信小程序,然后没,对,然后把界面要保持一致,跟前端的界面保持一致,然后用你那个框架,你自由去选,如果你知道框架,你就说不知道,你就框架自由去选择。 - -许永平 02:03:33 -要编辑我就告诉他,你把这个前端给我变成一起下。你直接就这样说,是吧?对,然后把界面。 - -卡若 02:03:53 -然后但是你的按钮要不要改变他有啊,项目怎么会没有呢? - -许永平 02:03:53 -难,难怪,难怪我说项目里面看不到,但是我们系统上有三个,我去把那个开发文档丢进去,他审。 - -卡若 02:03:59 -有了。 - -许永平 02:04:02 -没找到啊。 - -卡若 02:04:02 -找到啊。有,在这我就给你找。怎么会没有呢?小程序肯定有小程序的目录的,在这个等一下,这。 - -许永平 02:04:21 -对,那他是用什么技术去转成这一个小程序的? - -卡若 02:04:21 -对啊。就这个啊。他,他是用什么技术去转成这一个小程序的?他就直接翻译,自己翻译就行了。你是直接转让他转换,是吧? - -许永平 02:04:30 -你是直接转让他转换,是吗? - -卡若 02:04:32 -对,让他把 H5 的页面变成小程序,并且界面保持和所有功能保持一致啊。没了就一句话,你不用去思考它基础的问题,它转化不了,是它笨,骂它一顿。 - -许永平 02:04:38 -原来是这样,好。 - -卡若 02:04:44 -好。知道吧。今天 AI 就很笨。 - -许永平 02:04:47 -今天 KR 感觉最近都会降级,好多人都会说最近妈怎么对比你们用的也不好用。 - -卡若 02:04:50 -正。感觉最近都会降质,好多人都还说最近,我最近也用得也不好用。不好用是我跟你讲不好用,其实不是它降质的问题。是你问的问题太多,他积累很多经验,并且没有消化的经验,所以他就混乱了,他不知道你要 a 还是要b,还是要c。 - -许永平 02:05:09 -所以才会搞到这。 - -卡若 02:05:12 -因为我弄那个就有很离谱的问题,用的不是 ask 的模式,是用 agent 的模式,然后他一直回复我,让我手动去操作。 - -许永平 02:05:12 -今天我弄那个就很离谱的问题,用的是用对接的模式,然后他们说对,我们受不了。 - -卡若 02:05:23 -然后最后我受不了,我骂他一顿,我说你直接帮我改就好。对,你就说一顿,你特别是什么?你特别有一点。也是什么?他明明是 APP 模式,然后他回答你回复你说我现在是 APP 模式。 - -许永平 02:05:33 -他明明是 a 制模式,然后他回答你回复你说我现在是 b 制模式,我没办法。 - -卡若 02:05:37 -你你,你们加一句话,就是我一定要让他实现全自动化,并且不要让我任何操作的方式,那他会去解决,解决的时候他会告诉你我需要接口,我需要这个事情我做不了,需要去接口或者 API 或者那个secret。那你把这个给他,那他就实现全自动。 - -许永平 02:05:59 -因为这个是特别离谱的,第一次遇到。 - -卡若 02:05:59 -特别气人,他们只是第一次遇到。这个很正常的。 - -许永平 02:06:02 -我第一次遇到。 - -卡若 02:06:03 -我经常遇到,我就骂他。 - -许永平 02:06:05 -那一个。 - -卡若 02:06:06 -老王,那 MBTI 啥时候能上啊? - -许永平 02:06:06 -早上那一天。 - diff --git a/开发文档/当前小程序开发细则.md b/开发文档/当前小程序开发细则.md new file mode 100644 index 00000000..f953a9e4 --- /dev/null +++ b/开发文档/当前小程序开发细则.md @@ -0,0 +1,165 @@ +# 当前小程序开发细则 + +> 汇总当前 Soul 创业派对小程序的架构、经验与规划,便于新人上手与后续迭代。 +> 最后整理:2026-02 + +--- + +## 一、概述与定位 + +- **产品**:Soul 创业派对 — 微信小程序,内容为《一场 SOUL 的创业实验场》章节阅读 + 找伙伴 + 分销。 +- **技术**:原生微信小程序(非 uni-app / Taro),与 Next.js 后端同仓,接口统一走 `apiBase`(如 `https://soul.quwanzhi.com`)。 +- **核心能力**:章节阅读(免费/付费)、单章/全书支付、邀请码分销、找伙伴(匹配次数购买)、推广中心、我的订单/设置。 + +--- + +## 二、目录与页面结构 + +### 2.1 目录结构(miniprogram/) + +``` +miniprogram/ +├── app.js / app.json / app.wxss # 入口、全局配置、样式 +├── custom-tab-bar/ # 自定义底部 tabBar +├── pages/ +│ ├── index/ # 首页(推荐章节、已读统计) +│ ├── chapters/ # 目录(全书章节列表) +│ ├── read/ # 阅读页(核心:权限、支付、分享) +│ ├── match/ # 找伙伴(匹配 + 购买次数) +│ ├── my/ # 我的(入口:订单、推广、设置) +│ ├── referral/ # 推广中心(邀请码、收益、海报) +│ ├── purchases/ # 我的订单(当前为本地已购章节列表) +│ ├── settings/ # 设置(提现、绑定账号等) +│ ├── search/ # 搜索 +│ ├── about/ # 关于 +│ └── addresses/ # 地址(若启用) +├── utils/ +│ ├── chapterAccessManager.js # 章节权限与状态 +│ ├── readingTracker.js # 阅读进度追踪 +│ ├── payment.js # 旧支付封装(部分场景仍用) +│ └── util.js +└── assets/ # 图标等静态资源 +``` + +### 2.2 TabBar 与主要页面 + +| Tab | 页面 | 说明 | +|-----|------|------| +| 首页 | pages/index/index | 推荐章节、已读/待读、入口到阅读 | +| 目录 | pages/chapters/chapters | 全书章节列表 | +| 找伙伴 | pages/match/match | 匹配 + 购买匹配次数 | +| 我的 | pages/my/my | 已读/已购、推广中心、订单、设置 | + +非 Tab 页:阅读页 read、推广中心 referral、订单 purchases、设置 settings、搜索 search、关于 about。 + +### 2.3 全局状态(app.js globalData) + +- `userInfo`:登录后用户信息(id、openId、nickname、purchasedSections、hasFullBook、referralCode、referralCount 等)。 +- `openId` / `isLoggedIn`:微信登录态。 +- `readSectionIds`:已读章节 ID 列表(有权限打开全文时打点)。 +- `pendingReferralCode`:待绑定推荐码(带 ref 进入时写入,登录后绑定)。 +- `apiBase`:后端 API 根地址。 + +--- + +## 三、核心流程与经验 + +### 3.1 阅读与章节权限 + +- **权威数据源**:章节是否可读以**服务端**为准(`/api/user/check-purchased`、`/api/user/purchase-status`),不依赖前端缓存做最终判断。 +- **权限状态**:设计上支持 `unknown / free / locked_not_login / locked_not_purchased / unlocked_purchased / error`(见《章节阅读付费标准流程设计》);阅读页通过 `chapterAccessManager` 与 `determineAccessState` 等做状态判断。 +- **免费章节**:来自后端配置(如 `/api/db/config` 的 freeChapters),页面 onLoad 时拉取最新免费列表再判断;首帧可用本地默认 freeIds,拉取完成后需能刷新当前章状态,避免竞态误判。 +- **已读 vs 已购**: + - **已读**:仅统计“有权限打开并看到全文”的章节(`canAccess=true` 时 `app.markSectionAsRead(sectionId)`),存 `readSectionIds`。 + - **已购**:来自服务端 `userInfo.purchasedSections`、`hasFullBook` 及 orders 表,用于付费墙与购买按钮展示。 +- **登录后防误解锁**:登录成功(含支付流程中登录)后必须 `refreshPurchaseFromServer()`,再对当前章节做 `recheckCurrentSectionAndRefresh()`(先拉最新免费列表,再请求 check-purchased),避免“刚登录”时误用旧缓存解锁付费章。 +- **内容接口**:当前 `GET /api/book/chapter/[id]` 不校验登录与购买,前端按 `canAccess` 控制展示全文或预览;若未来开放 Web/API,建议章节接口侧做鉴权。 + +详见:`开发文档/8、部署/阅读逻辑分析.md`、`开发文档/8、部署/章节阅读付费标准流程设计.md`。 + +### 3.2 支付流程(章节 + 找伙伴) + +- **统一入口**:章节支付 `pages/read/read.js` 调 `POST /api/miniprogram/pay`;找伙伴支付 `pages/match/match.js` 同样调该接口,传 `productType: 'match'`。 +- **订单先行**:支付前**必须先创建订单**并插入 `orders` 表(status=`created`),再调微信统一下单;即使插库失败也继续支付流程,避免用户卡在“创建订单失败”。 +- **请求体约定**:`openId`、`userId`、`productType`(section/fullbook/match)、`productId`、`amount`、`description`;**必须带 `referralCode`**(见下节)。 +- **支付成功**:依赖微信回调 `POST /api/miniprogram/pay/notify` 更新订单为 `paid`、解锁用户权限、分佣、清理同产品未支付订单;前端支付成功后调用 `refreshUserPurchaseStatus()` 再 `initSection()` 刷新当前页。 +- **兜底**:若回调丢失,依赖**订单状态同步定时任务**(如每 5 分钟调 `GET /api/cron/sync-orders`)查询微信侧状态并同步到本地 orders,保证最终一致。详见 `开发文档/8、部署/订单状态同步定时任务.md`。 + +详见:`开发文档/8、部署/支付订单修复总结.md`、`开发文档/8、部署/支付订单完整修复方案.md`。 + +### 3.3 邀请码与分销(必传、必记) + +- **绑定逻辑**:带 `ref` 或 `referralCode` 的链接进入 → `app.js` 的 `handleReferralCode` 写入 `pendingReferralCode` 并**同步写入 `referral_code`**(`wx.setStorageSync('referral_code', refCode)`);登录后调用 `/api/referral/bind` 完成绑定(30 天有效、可续期/抢夺)。 +- **支付必带邀请码**:章节支付、找伙伴支付创建订单时都要传 `referralCode`,来源为 `wx.getStorageSync('referral_code')`;后端据此(或先查 referral_bindings)写入订单的 `referrer_id` 与 **`referral_code`**(下单时使用的邀请码,便于对账与后台展示)。 +- **订单表字段**:`orders.referrer_id`(推荐人用户ID)、`orders.referral_code`(下单时邀请码)。若表尚未加字段,需执行 `scripts/add_orders_referrer_id.py`、`scripts/add_orders_referral_code.py`。 +- **推荐人 vs 邀请码**:全局只认「推荐人 = 用户ID」;邀请码仅用于解析出 referrer_id,不会混用。分佣以 `referral_bindings` 为准,不依赖订单上的 referrer_id。详见 `开发文档/8、部署/邀请码分销规则说明.md`。 + +### 3.4 分享与落地 + +- 阅读页分享:`onShareAppMessage` / `onShareTimeline` 带 `id=章节ID&ref=当前用户邀请码`,落地后 ref 写入 storage,绑定与订单归属同上。 +- 文章/章节分销与全局同一套:不按“哪篇文章带来”单独分成或统计,仅按“谁发的链接(ref=谁)”归属。 + +--- + +## 四、数据与接口约定 + +### 4.1 关键接口 + +| 接口 | 用途 | +|------|------| +| POST /api/miniprogram/login | 微信登录,返回 userInfo(含 purchasedSections、referralCode 等) | +| GET /api/user/purchase-status | 拉取购买状态(已购章节、全书) | +| GET /api/user/check-purchased | 校验指定章节/全书是否已购买 | +| POST /api/miniprogram/pay | 创建订单 + 微信预支付,**需传 referralCode** | +| POST /api/miniprogram/pay/notify | 微信支付回调(服务端) | +| GET /api/cron/sync-orders | 订单状态同步(定时任务,需 secret) | +| POST /api/referral/bind | 绑定推荐关系 | +| GET /api/db/config | 免费章节等配置 | + +### 4.2 Storage 约定 + +- `referral_code`:落地 ref 或 app 检测到 ref 时写入;支付时读取并传后端。 +- `pendingReferralCode` / `boundReferralCode`:待绑定/已绑定推荐码(app 层)。 +- `readSectionIds`:已读章节 ID 列表。 +- `openId`、用户信息等由 app 与登录逻辑维护。 + +--- + +## 五、已知问题与修复要点 + +- **免费章节配置竞态**:onLoad 时若未 await 免费列表再 initSection,首帧可能用默认 freeIds 误判;建议先拉配置再判断当前章,或拉取完成后对当前页再刷一次权限。 +- **check-purchased 失败降级**:失败时应**保守**设为无权限(不信任本地缓存),避免误解锁。 +- **支付回调丢失**:必须部署订单同步定时任务(如宝塔 crontab 调 `/api/cron/sync-orders`),否则会出现“已扣款但订单仍 created、内容未解锁”。 +- **订单表缺字段**:新环境或老库需执行 `add_orders_referrer_id.py`、`add_orders_referral_code.py`,否则下单可能 fallback 成不写这两列(功能仍可用,但订单无推荐人/邀请码记录)。 + +--- + +## 六、部署与运维 + +- **后端**:Next.js 部署至 soul.quwanzhi.com,小程序 `app.globalData.apiBase` 指向该域名。 +- **订单同步**:生产环境配置 crontab 每 5 分钟请求 `GET /api/cron/sync-orders?secret=YOUR_SECRET`(如宝塔定时任务),保证回调丢失时仍能同步为 paid 并解锁。见 `开发文档/8、部署/订单状态同步定时任务.md`、`开发文档/8、部署/宝塔面板配置订单同步定时任务.md`。 +- **数据库**:orders 表需含 `referrer_id`、`referral_code`;若为已有表,执行上述两个 Python 迁移脚本。 +- **小程序发布**:按微信后台流程上传代码、提交审核;注意域名白名单、支付商户号与回调 URL 配置。 + +--- + +## 七、规划与待办(可选) + +- **我的订单页**:当前 `purchases` 页为本地已购章节列表;若需“真实订单列表+邀请码展示”,可对接 `GET /api/orders?userId=xxx` 并展示订单维度数据。 +- **阅读进度**:已设计阅读进度状态与 `readingTracker`、可上报服务端;是否全量接入与埋点可按产品需求推进。 +- **章节接口鉴权**:若开放 Web 或对外 API,建议在章节内容接口侧按用户与购买记录返回全文/预览,防止直连拿全文。 +- **按文章/章节维度的分销统计**:当前未实现;若需要“某章节带来的访问/订单数”,需在访问或订单上增加来源章节等字段并在报表中汇总。 + +--- + +## 八、相关文档索引 + +| 文档 | 说明 | +|------|------| +| 开发文档/8、部署/邀请码分销规则说明.md | 分销规则、订单 referrer_id/referral_code、推荐人 vs 邀请码 | +| 开发文档/8、部署/章节阅读付费标准流程设计.md | 阅读状态机、权限判断、阅读进度设计 | +| 开发文档/8、部署/阅读逻辑分析.md | 阅读流程结论、权限数据源、风险点 | +| 开发文档/8、部署/支付订单修复总结.md | 支付全流程与修复要点 | +| 开发文档/8、部署/订单状态同步定时任务.md | 回调兜底、cron 配置 | +| 开发文档/4、前端/ui/06-分销系统说明.md | 分销规则与推广方式 | +| 开发文档/4、前端/ui/08-支付系统说明.md | 支付方式与价格体系 |