暂存
This commit is contained in:
@@ -6,22 +6,58 @@
|
||||
const { parseScene } = require('./utils/scene.js')
|
||||
const { checkAndExecute } = require('./utils/ruleEngine.js')
|
||||
|
||||
const PRODUCTION_URL = 'https://soulapi.quwanzhi.com'
|
||||
const TEST_URL = 'https://souldev.quwanzhi.com'
|
||||
const LOCAL_URL = 'http://localhost:8080'
|
||||
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
||||
const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
|
||||
function getRuntimeBootstrapConfig() {
|
||||
try {
|
||||
const accountInfo = wx.getAccountInfoSync?.()
|
||||
const envVersion = accountInfo?.miniProgram?.envVersion || 'release'
|
||||
const extCfg = wx.getExtConfigSync ? (wx.getExtConfigSync() || {}) : {}
|
||||
// 按运行环境自动切换 API 地址
|
||||
let baseUrl
|
||||
if (envVersion === 'release') {
|
||||
baseUrl = PRODUCTION_URL
|
||||
} else if (envVersion === 'trial') {
|
||||
baseUrl = extCfg.apiBaseUrl || wx.getStorageSync('apiBaseUrl') || TEST_URL
|
||||
} else {
|
||||
// develop:不使用 storage,避免被 loadMpConfig 曾写入的生产地址污染;env-switch 仍可运行时覆盖
|
||||
baseUrl = extCfg.apiBaseUrl || LOCAL_URL
|
||||
}
|
||||
return {
|
||||
baseUrl,
|
||||
appId: extCfg.appId || DEFAULT_APP_ID,
|
||||
mchId: extCfg.mchId || DEFAULT_MCH_ID,
|
||||
withdrawSubscribeTmplId: extCfg.withdrawSubscribeTmplId || DEFAULT_WITHDRAW_TMPL_ID
|
||||
}
|
||||
} catch (_) {
|
||||
return {
|
||||
baseUrl: PRODUCTION_URL,
|
||||
appId: DEFAULT_APP_ID,
|
||||
mchId: DEFAULT_MCH_ID,
|
||||
withdrawSubscribeTmplId: DEFAULT_WITHDRAW_TMPL_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapConfig = getRuntimeBootstrapConfig()
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
// API 基础地址(切换环境时注释/取消注释)
|
||||
// baseUrl: 'https://soulapi.quwanzhi.com',
|
||||
baseUrl: 'http://localhost:8080', // 本地调试
|
||||
// baseUrl: 'https://souldev.quwanzhi.com', // 测试环境
|
||||
|
||||
|
||||
// API 基础地址:优先外部配置/缓存,其次默认生产环境
|
||||
baseUrl: bootstrapConfig.baseUrl,
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: 'wxb8bbb2b10dec74aa',
|
||||
appId: bootstrapConfig.appId,
|
||||
|
||||
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
|
||||
withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
|
||||
withdrawSubscribeTmplId: bootstrapConfig.withdrawSubscribeTmplId,
|
||||
|
||||
// 微信支付配置
|
||||
mchId: '1318592501', // 商户号
|
||||
mchId: bootstrapConfig.mchId,
|
||||
|
||||
// 用户信息
|
||||
userInfo: null,
|
||||
@@ -67,7 +103,16 @@ App({
|
||||
isSinglePageMode: false,
|
||||
|
||||
// 更新检测:上次检测时间戳,避免频繁请求
|
||||
lastUpdateCheck: 0
|
||||
lastUpdateCheck: 0,
|
||||
// mpConfig 上次刷新时间戳(onShow 节流,避免频繁请求)
|
||||
lastMpConfigCheck: 0,
|
||||
|
||||
// 审核模式:后端 /api/miniprogram/config 返回 auditMode=true 时隐藏所有支付相关UI
|
||||
auditMode: false,
|
||||
// 客服/微信:mp_config 返回 supportWechat
|
||||
supportWechat: '',
|
||||
// API 域名:loadMpConfig 从 config 更新
|
||||
apiDomain: ''
|
||||
},
|
||||
|
||||
onLaunch(options) {
|
||||
@@ -102,10 +147,16 @@ App({
|
||||
this.handleReferralCode(options)
|
||||
},
|
||||
|
||||
// 小程序显示时:处理分享参数、检测更新(从后台切回时)
|
||||
// 小程序显示时:处理分享参数、检测更新、刷新 mpConfig(从后台切回时)
|
||||
onShow(options) {
|
||||
this.handleReferralCode(options)
|
||||
this.checkUpdate()
|
||||
// 从后台切回时刷新审核模式等配置(节流 30 秒,避免频繁请求)
|
||||
const now = Date.now()
|
||||
if (!this.globalData.lastMpConfigCheck || now - this.globalData.lastMpConfigCheck > 30 * 1000) {
|
||||
this.globalData.lastMpConfigCheck = now
|
||||
this.loadMpConfig()
|
||||
}
|
||||
},
|
||||
|
||||
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环
|
||||
@@ -330,15 +381,35 @@ App({
|
||||
}
|
||||
},
|
||||
|
||||
// 加载 mpConfig(appId、mchId、withdrawSubscribeTmplId 等),失败时保留 globalData 默认值
|
||||
// 加载 mpConfig(appId、mchId、withdrawSubscribeTmplId、auditMode、supportWechat、apiDomain 等),失败时保留 globalData 默认值
|
||||
async loadMpConfig() {
|
||||
try {
|
||||
const res = await this.request({ url: '/api/miniprogram/config', silent: true })
|
||||
const res = await this.request({ url: '/api/miniprogram/config', silent: true, timeout: 5000 })
|
||||
const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config)
|
||||
if (mp && typeof mp === 'object') {
|
||||
if (mp.appId) this.globalData.appId = mp.appId
|
||||
if (mp.mchId) this.globalData.mchId = mp.mchId
|
||||
if (mp.withdrawSubscribeTmplId) this.globalData.withdrawSubscribeTmplId = mp.withdrawSubscribeTmplId
|
||||
// 仅正式版使用后端 apiDomain;开发/体验版保持 bootstrap 的 baseUrl,避免被生产地址覆盖
|
||||
try {
|
||||
const envVersion = wx.getAccountInfoSync?.()?.miniProgram?.envVersion || 'release'
|
||||
if (envVersion === 'release' && mp.apiDomain) {
|
||||
this.globalData.baseUrl = mp.apiDomain
|
||||
this.globalData.apiDomain = mp.apiDomain
|
||||
try { wx.setStorageSync('apiBaseUrl', mp.apiDomain) } catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
this.globalData.auditMode = !!mp.auditMode
|
||||
this.globalData.supportWechat = mp.supportWechat || mp.customerWechat || mp.serviceWechat || ''
|
||||
// 通知当前已加载的页面刷新 auditMode(从后台切回时配置更新后立即生效)
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
pages.forEach(p => {
|
||||
if (p && p.data && 'auditMode' in p.data) {
|
||||
p.setData({ auditMode: this.globalData.auditMode || false })
|
||||
}
|
||||
})
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[App] loadMpConfig 失败,使用默认值:', e?.message || e)
|
||||
@@ -426,6 +497,7 @@ App({
|
||||
url: this.globalData.baseUrl + url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
timeout: options.timeout || 15000,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
|
||||
@@ -59,7 +59,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"usingComponents": {},
|
||||
"usingComponents": {
|
||||
"env-switch": "/components/env-switch/env-switch"
|
||||
},
|
||||
"navigateToMiniProgramAppIdList": [
|
||||
"wx6489c26045912fe1",
|
||||
"wx3d15ed02e98b04e3"
|
||||
|
||||
82
miniprogram/components/env-switch/env-switch.js
Normal file
82
miniprogram/components/env-switch/env-switch.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 开发环境专用:可拖拽的 baseURL 切换悬浮按钮
|
||||
* 正式环境(release)不显示
|
||||
*/
|
||||
const PRODUCTION_URL = 'https://soulapi.quwanzhi.com'
|
||||
const STORAGE_KEY = 'apiBaseUrl'
|
||||
const POSITION_KEY = 'envSwitchPosition'
|
||||
|
||||
const URL_OPTIONS = [
|
||||
{ label: '生产', url: PRODUCTION_URL },
|
||||
{ label: '本地', url: 'http://localhost:8080' },
|
||||
{ label: '测试', url: 'https://souldev.quwanzhi.com' },
|
||||
]
|
||||
|
||||
Component({
|
||||
data: {
|
||||
visible: false,
|
||||
x: 20,
|
||||
y: 120,
|
||||
currentLabel: '生产',
|
||||
areaWidth: 375,
|
||||
areaHeight: 812,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
try {
|
||||
const accountInfo = wx.getAccountInfoSync?.()
|
||||
const envVersion = accountInfo?.miniProgram?.envVersion || 'release'
|
||||
if (envVersion === 'release') {
|
||||
return
|
||||
}
|
||||
const sys = wx.getSystemInfoSync?.() || {}
|
||||
const areaWidth = sys.windowWidth || 375
|
||||
const areaHeight = sys.windowHeight || 812
|
||||
const saved = wx.getStorageSync(POSITION_KEY)
|
||||
const pos = saved ? JSON.parse(saved) : { x: 20, y: 120 }
|
||||
// 与 app.js 一致:storage 优先,否则用 globalData(已按 env 自动切换)
|
||||
const current = wx.getStorageSync(STORAGE_KEY) || getApp().globalData?.baseUrl || PRODUCTION_URL
|
||||
const opt = URL_OPTIONS.find(o => o.url === current) || URL_OPTIONS[0]
|
||||
this.setData({
|
||||
visible: true,
|
||||
x: pos.x ?? 20,
|
||||
y: pos.y ?? 120,
|
||||
currentLabel: opt.label,
|
||||
areaWidth,
|
||||
areaHeight,
|
||||
})
|
||||
} catch (_) {
|
||||
this.setData({ visible: false })
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap() {
|
||||
const items = URL_OPTIONS.map(o => o.label)
|
||||
const current = wx.getStorageSync(STORAGE_KEY) || PRODUCTION_URL
|
||||
const idx = URL_OPTIONS.findIndex(o => o.url === current)
|
||||
wx.showActionSheet({
|
||||
itemList: items,
|
||||
success: (res) => {
|
||||
const opt = URL_OPTIONS[res.tapIndex]
|
||||
wx.setStorageSync(STORAGE_KEY, opt.url)
|
||||
const app = getApp()
|
||||
if (app && app.globalData) {
|
||||
app.globalData.baseUrl = opt.url
|
||||
}
|
||||
this.setData({ currentLabel: opt.label })
|
||||
wx.showToast({ title: `已切到${opt.label}`, icon: 'none', duration: 1500 })
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onMovableChange(e) {
|
||||
const { x, y } = e.detail
|
||||
if (typeof x === 'number' && typeof y === 'number') {
|
||||
wx.setStorageSync(POSITION_KEY, JSON.stringify({ x, y }))
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
3
miniprogram/components/env-switch/env-switch.json
Normal file
3
miniprogram/components/env-switch/env-switch.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
13
miniprogram/components/env-switch/env-switch.wxml
Normal file
13
miniprogram/components/env-switch/env-switch.wxml
Normal file
@@ -0,0 +1,13 @@
|
||||
<movable-area wx:if="{{visible}}" class="env-area" style="width:{{areaWidth}}px;height:{{areaHeight}}px;">
|
||||
<movable-view
|
||||
class="env-btn"
|
||||
direction="all"
|
||||
inertia
|
||||
x="{{x}}"
|
||||
y="{{y}}"
|
||||
bindchange="onMovableChange"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view class="env-btn-inner">{{currentLabel}}</view>
|
||||
</movable-view>
|
||||
</movable-area>
|
||||
30
miniprogram/components/env-switch/env-switch.wxss
Normal file
30
miniprogram/components/env-switch/env-switch.wxss
Normal file
@@ -0,0 +1,30 @@
|
||||
.env-area {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.env-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.env-btn-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(34, 197, 94, 0.4);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
@@ -40,9 +40,6 @@ Page({
|
||||
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源' }
|
||||
],
|
||||
|
||||
// 每日新增章节(懒加载后暂无,可后续用 latest-chapters 补充)
|
||||
dailyChapters: [],
|
||||
|
||||
// book/parts 加载中
|
||||
partsLoading: true,
|
||||
|
||||
@@ -59,7 +56,6 @@ Page({
|
||||
this.updateUserStatus()
|
||||
this.loadVipStatus()
|
||||
this.loadParts()
|
||||
this.loadDailyChapters()
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
|
||||
@@ -197,36 +193,11 @@ Page({
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
Promise.all([this.loadParts(), this.loadDailyChapters()])
|
||||
this.loadParts()
|
||||
.then(() => wx.stopPullDownRefresh())
|
||||
.catch(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
// 每日新增:用 latest-chapters 接口,展示最近更新章节
|
||||
async loadDailyChapters() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
|
||||
const list = (res && res.data) ? res.data : []
|
||||
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
|
||||
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
|
||||
const daily = list
|
||||
.filter(exclude)
|
||||
.slice(0, 10)
|
||||
.map(c => {
|
||||
const d = new Date(c.updatedAt || c.updated_at || Date.now())
|
||||
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
|
||||
return {
|
||||
id: c.id,
|
||||
mid: c.mid ?? c.MID ?? 0,
|
||||
title,
|
||||
price: c.price ?? 1,
|
||||
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
})
|
||||
this.setData({ dailyChapters: daily })
|
||||
} catch (e) { console.log('[Chapters] 加载每日新增失败:', e) }
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
|
||||
@@ -40,31 +40,6 @@
|
||||
|
||||
<!-- 目录内容 -->
|
||||
<view class="chapters-content" wx:if="{{!partsLoading}}">
|
||||
<!-- 每日新增(最近更新章节快捷入口) -->
|
||||
<view class="daily-section" wx:if="{{dailyChapters.length > 0}}">
|
||||
<view class="daily-header">
|
||||
<text class="daily-title">每日新增</text>
|
||||
<text class="daily-badge">+{{dailyChapters.length}}</text>
|
||||
</view>
|
||||
<view class="daily-list">
|
||||
<view
|
||||
class="daily-item"
|
||||
wx:for="{{dailyChapters}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="daily-dot"></view>
|
||||
<view class="daily-content">
|
||||
<text class="daily-item-title">{{item.title}}</text>
|
||||
<text class="daily-item-meta">{{item.dateStr}} · ¥{{item.price}}</text>
|
||||
</view>
|
||||
<text class="daily-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 序言(优先传 mid,阅读页用 by-mid 请求) -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="preface" data-mid="{{fixedSectionsMap.preface}}">
|
||||
<view class="item-left">
|
||||
@@ -158,4 +133,5 @@
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -174,89 +174,6 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===== 每日新增 ===== */
|
||||
.daily-section {
|
||||
margin-bottom: 32rpx;
|
||||
padding: 24rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.daily-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.daily-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.daily-badge {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
background: #F6AD55;
|
||||
color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.daily-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.daily-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.daily-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.daily-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 206, 209, 0.6);
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.daily-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.daily-item-title {
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.daily-item-meta {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.daily-arrow {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== 章节项 ===== */
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
@@ -609,21 +526,6 @@
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ===== 每日新增章节 ===== */
|
||||
.daily-section { margin: 20rpx 0; padding: 24rpx; background: rgba(255,215,0,0.04); border: 1rpx solid rgba(255,215,0,0.12); border-radius: 16rpx; }
|
||||
.daily-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.daily-title { font-size: 30rpx; font-weight: 600; color: #FFD700; }
|
||||
.daily-badge { font-size: 22rpx; background: #FFD700; color: #000; padding: 2rpx 12rpx; border-radius: 20rpx; font-weight: bold; }
|
||||
.daily-list { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.daily-item { display: flex; justify-content: space-between; align-items: center; padding: 16rpx; background: rgba(255,255,255,0.03); border-radius: 12rpx; }
|
||||
.daily-left { display: flex; align-items: center; gap: 10rpx; flex: 1; min-width: 0; }
|
||||
.daily-new-tag { font-size: 18rpx; background: #FF4444; color: #fff; padding: 2rpx 8rpx; border-radius: 6rpx; font-weight: bold; flex-shrink: 0; }
|
||||
.daily-item-title { font-size: 26rpx; color: rgba(255,255,255,0.85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.daily-right { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
|
||||
.daily-price { font-size: 26rpx; color: #FFD700; font-weight: 600; }
|
||||
.daily-date { font-size: 20rpx; color: rgba(255,255,255,0.35); }
|
||||
.daily-note { display: block; font-size: 22rpx; color: rgba(255,215,0,0.5); margin-top: 12rpx; text-align: center; }
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
|
||||
@@ -106,4 +106,5 @@
|
||||
<view class="bg-glow bg-glow-2"></view>
|
||||
<view class="bg-dots"></view>
|
||||
</view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
console.log('[Index] ===== 首页文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -68,7 +69,10 @@ Page({
|
||||
featuredExpandedLoading: false,
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true
|
||||
searchEnabled: true,
|
||||
|
||||
// 审核模式:隐藏支付相关入口
|
||||
auditMode: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -93,6 +97,7 @@ Page({
|
||||
|
||||
onShow() {
|
||||
console.log('[Index] onShow 触发')
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
@@ -309,35 +314,45 @@ Page({
|
||||
|
||||
// 跳转到目录
|
||||
goToChapters() {
|
||||
trackClick('home', 'nav_click', '阅读进度')
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
|
||||
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
|
||||
const hasCachedFeatures = app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean'
|
||||
if (hasCachedFeatures) {
|
||||
this.setData({
|
||||
searchEnabled: app.globalData.features.searchEnabled,
|
||||
auditMode: app.globalData.auditMode || false
|
||||
})
|
||||
return
|
||||
}
|
||||
const res = await app.request({ url: '/api/miniprogram/config', silent: true })
|
||||
const features = (res && res.features) || {}
|
||||
const mp = (res && res.mpConfig) || {}
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
const auditMode = !!mp.auditMode
|
||||
if (!app.globalData.features) app.globalData.features = {}
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
this.setData({ searchEnabled })
|
||||
app.globalData.auditMode = auditMode
|
||||
this.setData({ searchEnabled, auditMode })
|
||||
} catch (e) {
|
||||
this.setData({ searchEnabled: true })
|
||||
this.setData({ searchEnabled: true, auditMode: app.globalData.auditMode || false })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
if (!this.data.searchEnabled) return
|
||||
trackClick('home', 'nav_click', '搜索')
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
trackClick('home', 'card_click', id || '章节')
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
@@ -349,10 +364,12 @@ Page({
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
trackClick('home', 'btn_click', '加入创业派对')
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
async onLinkKaruo() {
|
||||
trackClick('home', 'btn_click', '链接卡若')
|
||||
const app = getApp()
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
wx.showModal({
|
||||
@@ -528,6 +545,7 @@ Page({
|
||||
// 精选推荐:展开/折叠
|
||||
async toggleFeaturedExpanded() {
|
||||
if (this.data.featuredExpandedLoading) return
|
||||
trackClick('home', 'tab_click', this.data.featuredExpanded ? '精选收起' : '精选展开')
|
||||
if (this.data.featuredExpanded) {
|
||||
const collapsed = this.data.featuredSectionsFull.length > 0 ? this.data.featuredSectionsFull.slice(0, 3) : this.data.featuredSections
|
||||
this.setData({ featuredExpanded: false, featuredSections: collapsed })
|
||||
@@ -564,6 +582,7 @@ Page({
|
||||
|
||||
// 最新新增:展开/折叠(默认 5 条,点击展开剩余)
|
||||
toggleLatestExpanded() {
|
||||
trackClick('home', 'tab_click', this.data.latestExpanded ? '最新收起' : '最新展开')
|
||||
const expanded = !this.data.latestExpanded
|
||||
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
|
||||
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
|
||||
@@ -598,6 +617,7 @@ Page({
|
||||
|
||||
goToMemberDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
trackClick('home', 'card_click', '超级个体_' + (id || ''))
|
||||
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${id}` })
|
||||
},
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 超级个体(横向滚动,已去掉「查看全部」) -->
|
||||
<view class="section">
|
||||
<!-- 超级个体(横向滚动,已去掉「查看全部」;审核模式隐藏) -->
|
||||
<view class="section" wx:if="{{!auditMode}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">超级个体</text>
|
||||
</view>
|
||||
@@ -190,4 +190,5 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
const app = getApp()
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
// 默认匹配类型配置
|
||||
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
|
||||
@@ -198,6 +199,7 @@ Page({
|
||||
// 选择匹配类型
|
||||
selectType(e) {
|
||||
const typeId = e.currentTarget.dataset.type
|
||||
trackClick('match', 'tab_click', typeId || '类型')
|
||||
const type = MATCH_TYPES.find(t => t.id === typeId)
|
||||
this.setData({
|
||||
selectedType: typeId,
|
||||
@@ -207,6 +209,7 @@ Page({
|
||||
|
||||
// 点击匹配按钮
|
||||
async handleMatchClick() {
|
||||
trackClick('match', 'btn_click', '匹配_' + (this.data.selectedType || ''))
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
// 导师顾问:先播匹配动画,动画完成后再跳转(不在此处直接跳)
|
||||
|
||||
@@ -325,4 +325,5 @@
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
const app = getApp()
|
||||
const { formatStatNum } = require('../../utils/util.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -39,6 +40,7 @@ Page({
|
||||
// 功能配置
|
||||
matchEnabled: false,
|
||||
referralEnabled: true,
|
||||
auditMode: false,
|
||||
searchEnabled: true,
|
||||
|
||||
// VIP状态
|
||||
@@ -98,6 +100,7 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
// 设置TabBar选中状态(根据 matchEnabled 动态设置)
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
@@ -118,8 +121,11 @@ Page({
|
||||
const matchEnabled = features.matchEnabled === true
|
||||
const referralEnabled = features.referralEnabled !== false
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
const mp = (res && res.mpConfig) || {}
|
||||
const auditMode = !!mp.auditMode
|
||||
app.globalData.auditMode = auditMode
|
||||
app.globalData.features = { matchEnabled, referralEnabled, searchEnabled }
|
||||
this.setData({ matchEnabled, referralEnabled, searchEnabled })
|
||||
this.setData({ matchEnabled, referralEnabled, searchEnabled, auditMode })
|
||||
} catch (error) {
|
||||
console.log('加载功能配置失败:', error)
|
||||
this.setData({ matchEnabled: false, referralEnabled: true, searchEnabled: true })
|
||||
@@ -322,6 +328,7 @@ Page({
|
||||
|
||||
// 一键收款:逐条调起微信收款页(有上一页则返回,无则回首页)
|
||||
async handleOneClickReceive() {
|
||||
trackClick('my', 'btn_click', '一键收款')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
if (this.data.receivingAll) return
|
||||
|
||||
@@ -664,6 +671,7 @@ Page({
|
||||
|
||||
// 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
|
||||
showLogin() {
|
||||
trackClick('my', 'btn_click', '点击登录')
|
||||
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
@@ -764,6 +772,7 @@ Page({
|
||||
// 点击菜单
|
||||
handleMenuTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
trackClick('my', 'nav_click', id || '菜单')
|
||||
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
@@ -787,6 +796,7 @@ Page({
|
||||
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
trackClick('my', 'card_click', id || '章节')
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
@@ -794,16 +804,19 @@ Page({
|
||||
|
||||
// 跳转到目录
|
||||
goToChapters() {
|
||||
trackClick('my', 'nav_click', '已读章节')
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 跳转到匹配
|
||||
goToMatch() {
|
||||
trackClick('my', 'nav_click', '匹配伙伴')
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 跳转到推广中心(需登录)
|
||||
goToReferral() {
|
||||
trackClick('my', 'nav_click', '推广中心')
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
@@ -919,18 +932,21 @@ Page({
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
trackClick('my', 'btn_click', '会员中心')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
// 进入个人资料编辑页(stitch_soul)
|
||||
goToProfileEdit() {
|
||||
trackClick('my', 'nav_click', '资料编辑')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 进入个人资料展示页(enhanced_professional_profile),展示页内可再进编辑
|
||||
goToProfileShow() {
|
||||
trackClick('my', 'btn_click', '编辑')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/profile-show/profile-show' })
|
||||
},
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
<image class="profile-edit-icon" src="/assets/icons/edit-gray.svg" mode="aspectFit"/>
|
||||
<text class="profile-edit-text">编辑</text>
|
||||
</view>
|
||||
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
|
||||
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" wx:if="{{!auditMode}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="vip-tags">
|
||||
<view class="vip-tags" wx:if="{{!auditMode}}">
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToMatch">匹配</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">排行</text>
|
||||
@@ -63,7 +63,7 @@
|
||||
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
<view class="profile-stat" bindtap="handleMenuTap" data-id="wallet">
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
<text class="profile-stat-val">{{walletBalanceText}}</text>
|
||||
<text class="profile-stat-label">我的余额</text>
|
||||
</view>
|
||||
@@ -73,8 +73,8 @@
|
||||
|
||||
<!-- 已登录:内容区 -->
|
||||
<view class="main-content" wx:if="{{isLoggedIn}}">
|
||||
<!-- 一键收款(仅在有待确认收款时显示) -->
|
||||
<view class="card receive-card" wx:if="{{pendingConfirmList.length > 0}}">
|
||||
<!-- 一键收款(仅在有待确认收款时显示;审核模式隐藏) -->
|
||||
<view class="card receive-card" wx:if="{{pendingConfirmList.length > 0 && !auditMode}}">
|
||||
<view class="receive-top">
|
||||
<view class="receive-left">
|
||||
<view class="receive-title-row">
|
||||
@@ -253,4 +253,5 @@
|
||||
</view>
|
||||
|
||||
<view class="bottom-space"></view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -77,20 +77,29 @@ Page({
|
||||
|
||||
// 余额(用于余额支付)
|
||||
walletBalance: 0,
|
||||
|
||||
// 审核模式:隐藏购买按钮
|
||||
auditMode: false,
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
|
||||
// 预加载 linkTags、linkedMiniprograms(供 onLinkTagTap 用密钥查 appId)
|
||||
if (!app.globalData.linkTagsConfig || !app.globalData.linkedMiniprograms) {
|
||||
app.request({ url: '/api/miniprogram/config', silent: true }).then(cfg => {
|
||||
if (cfg) {
|
||||
if (Array.isArray(cfg.linkTags)) app.globalData.linkTagsConfig = cfg.linkTags
|
||||
if (Array.isArray(cfg.linkedMiniprograms)) app.globalData.linkedMiniprograms = cfg.linkedMiniprograms
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
// 预加载 config:linkTags、auditMode 等(阅读页直接进入时需主动拉取最新审核状态)
|
||||
app.request({ url: '/api/miniprogram/config', silent: true }).then(cfg => {
|
||||
if (cfg) {
|
||||
if (Array.isArray(cfg.linkTags)) app.globalData.linkTagsConfig = cfg.linkTags
|
||||
if (Array.isArray(cfg.linkedMiniprograms)) app.globalData.linkedMiniprograms = cfg.linkedMiniprograms
|
||||
const mp = (cfg && cfg.mpConfig) || {}
|
||||
const auditMode = !!mp.auditMode
|
||||
app.globalData.auditMode = auditMode
|
||||
if (typeof this.setData === 'function') this.setData({ auditMode })
|
||||
}
|
||||
}).catch(() => {})
|
||||
|
||||
// 支持 scene(扫码)、mid、id、ref、gift(代付)
|
||||
const sceneStr = (options && options.scene) || ''
|
||||
@@ -749,6 +758,33 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 右下角悬浮按钮:分享到朋友圈(复制文案 + 引导点右上角)
|
||||
shareToMoments() {
|
||||
const title = this.data.section?.title || this.data.chapterTitle || '好文推荐'
|
||||
const raw = (this.data.content || '')
|
||||
.replace(/<[^>]+>/g, '\n')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"')
|
||||
.replace(/[#@]\S+/g, '')
|
||||
const sentences = raw.split(/[。!?\n]+/).map(s => s.trim()).filter(s => s.length > 4)
|
||||
const picked = sentences.slice(0, 5)
|
||||
const copyText = picked.length > 0 ? title + '\n\n' + picked.join('\n\n') : `🔥 刚看完这篇《${title}》,推荐给你!\n\n#Soul创业派对 #真实商业故事`
|
||||
wx.setClipboardData({
|
||||
data: copyText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '文案已复制',
|
||||
content: '请点击右上角「···」菜单,选择「分享到朋友圈」即可发布',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '复制失败,请手动复制', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 分享到朋友圈:带文章标题,过长时截断
|
||||
onShareTimeline() {
|
||||
const { section, sectionId, sectionMid, chapterTitle } = this.data
|
||||
|
||||
@@ -93,11 +93,14 @@
|
||||
<text class="action-icon-small">🖼️</text>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn}}">
|
||||
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<text class="action-icon-small">🎁</text>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="share-tip-inline" wx:if="{{!auditMode}}">
|
||||
<text class="share-tip-text">分享后好友购买,你可获得 90% 收益</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -169,8 +172,8 @@
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读50%,购买后继续阅读</text>
|
||||
|
||||
<!-- 购买选项 -->
|
||||
<view class="purchase-options">
|
||||
<!-- 购买选项(审核模式隐藏) -->
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<!-- 购买本章 - 直接调起支付 -->
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
@@ -189,10 +192,11 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
|
||||
<text class="paywall-tip">分享给好友一起学习,还能赚取佣金</text>
|
||||
<!-- 代付分享:让好友帮我买 -->
|
||||
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn}}">
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
<!-- 代付分享:让好友帮我买(审核模式隐藏) -->
|
||||
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<text class="gift-share-icon">🎁</text>
|
||||
<text class="gift-share-text">找好友代付</text>
|
||||
</view>
|
||||
@@ -306,8 +310,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右下角悬浮分享按钮 -->
|
||||
<button class="fab-share" open-type="share">
|
||||
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
</button>
|
||||
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
|
||||
<view class="fab-share" bindtap="shareToMoments">
|
||||
<text class="fab-moments-icon">🌐</text>
|
||||
</view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -336,6 +336,12 @@
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
.paywall-audit-tip {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
/* ===== 章节导航 ===== */
|
||||
.chapter-nav {
|
||||
@@ -475,6 +481,15 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.share-tip-inline {
|
||||
margin-top: 16rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.share-tip-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* ===== 推广提示区 ===== */
|
||||
.promo-section {
|
||||
margin-top: 32rpx;
|
||||
@@ -1051,3 +1066,8 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fab-moments-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* - 收益统计(90%归分发者)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -258,6 +259,7 @@ Page({
|
||||
// 切换Tab
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
trackClick('referral', 'tab_click', tab || '绑定列表')
|
||||
let currentBindings = []
|
||||
|
||||
if (tab === 'active') {
|
||||
@@ -278,6 +280,7 @@ Page({
|
||||
|
||||
// 复制邀请链接
|
||||
copyLink() {
|
||||
trackClick('referral', 'btn_click', '复制链接')
|
||||
const link = `https://soul.quwanzhi.com/?ref=${this.data.referralCode}`
|
||||
wx.setClipboardData({
|
||||
data: link,
|
||||
@@ -287,6 +290,7 @@ Page({
|
||||
|
||||
// 分享到朋友圈 - 1:1 迁移 Next.js 的 handleShareToWechat
|
||||
shareToWechat() {
|
||||
trackClick('referral', 'btn_click', '分享朋友圈')
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
@@ -314,6 +318,7 @@ Page({
|
||||
|
||||
// 更多分享方式 - 1:1 迁移 Next.js 的 handleShare
|
||||
handleMoreShare() {
|
||||
trackClick('referral', 'btn_click', '更多分享')
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
@@ -334,6 +339,7 @@ Page({
|
||||
|
||||
// 生成推广海报 - 1:1 对齐 Next.js 设计
|
||||
async generatePoster() {
|
||||
trackClick('referral', 'btn_click', '生成海报')
|
||||
wx.showLoading({ title: '生成中...', mask: true })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
@@ -624,6 +630,7 @@ Page({
|
||||
|
||||
// 提现 - 直接到微信零钱
|
||||
async handleWithdraw() {
|
||||
trackClick('referral', 'btn_click', '申请提现')
|
||||
const availableEarnings = this.data.availableEarningsNum || 0
|
||||
const minWithdrawAmount = this.data.minWithdrawAmount || 10
|
||||
const hasWechatId = this.data.hasWechatId
|
||||
@@ -670,6 +677,7 @@ Page({
|
||||
|
||||
// 跳转提现记录页
|
||||
goToWithdrawRecords() {
|
||||
trackClick('referral', 'nav_click', '提现记录')
|
||||
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
|
||||
},
|
||||
|
||||
|
||||
@@ -329,4 +329,5 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 搜索章节标题和内容
|
||||
*/
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -71,6 +72,7 @@ Page({
|
||||
// 点击热门关键词
|
||||
onHotKeyword(e) {
|
||||
const keyword = e.currentTarget.dataset.keyword
|
||||
trackClick('search', 'tab_click', keyword || '关键词')
|
||||
this.setData({ keyword })
|
||||
this.doSearch()
|
||||
},
|
||||
@@ -78,6 +80,7 @@ Page({
|
||||
// 执行搜索
|
||||
async doSearch() {
|
||||
const { keyword } = this.data
|
||||
if (keyword && keyword.trim().length >= 1) trackClick('search', 'btn_click', '搜索_' + keyword.trim())
|
||||
if (!keyword || keyword.trim().length < 1) {
|
||||
wx.showToast({ title: '请输入搜索关键词', icon: 'none' })
|
||||
return
|
||||
@@ -108,6 +111,7 @@ Page({
|
||||
// 跳转阅读(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
trackClick('search', 'card_click', id || '章节')
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
|
||||
@@ -112,4 +112,5 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -177,4 +177,5 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -64,6 +65,7 @@ Page({
|
||||
},
|
||||
|
||||
async handlePurchase() {
|
||||
trackClick('vip', 'btn_click', '开通VIP')
|
||||
let userId = app.globalData.userInfo?.id
|
||||
let openId = app.globalData.openId || app.globalData.userInfo?.open_id
|
||||
if (!userId || !openId) {
|
||||
|
||||
@@ -52,4 +52,5 @@
|
||||
</view>
|
||||
|
||||
<view class="bottom-space"></view>
|
||||
<env-switch />
|
||||
</view>
|
||||
@@ -10,14 +10,22 @@ Page({
|
||||
loading: true,
|
||||
rechargeAmounts: [10, 30, 50, 100],
|
||||
selectedAmount: 30,
|
||||
auditMode: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
auditMode: app.globalData.auditMode || false,
|
||||
})
|
||||
this.loadBalance()
|
||||
this.loadTransactions()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
},
|
||||
|
||||
async loadBalance() {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
|
||||
const userId = app.globalData.userInfo.id
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section" wx:if="{{!auditMode}}">
|
||||
<view class="section-head">
|
||||
<text class="section-title">选择充值金额</text>
|
||||
<text class="section-note">当前已选 ¥{{selectedAmount}}</text>
|
||||
@@ -44,9 +44,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<view class="action-row" wx:if="{{!auditMode}}">
|
||||
<view class="btn btn-recharge" bindtap="handleRecharge">充值</view>
|
||||
</view>
|
||||
<view class="action-row" wx:elif="{{auditMode}}">
|
||||
<view class="audit-tip">审核中,暂不支持充值</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
@@ -75,4 +78,5 @@
|
||||
</view>
|
||||
|
||||
<view class="bottom-space"></view>
|
||||
<env-switch />
|
||||
</view>
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
line-height: 1.7;
|
||||
color: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
.audit-tip {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
}
|
||||
.balance-skeleton {
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
1
soul-admin/dist/assets/index-BcWuvM_a.css
vendored
1
soul-admin/dist/assets/index-BcWuvM_a.css
vendored
File diff suppressed because one or more lines are too long
914
soul-admin/dist/assets/index-D5RkA1Qc.js
vendored
914
soul-admin/dist/assets/index-D5RkA1Qc.js
vendored
File diff suppressed because one or more lines are too long
914
soul-admin/dist/assets/index-DyqIjjBz.js
vendored
Normal file
914
soul-admin/dist/assets/index-DyqIjjBz.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-o3d5k2lQ.css
vendored
Normal file
1
soul-admin/dist/assets/index-o3d5k2lQ.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Soul创业派对</title>
|
||||
<script type="module" crossorigin src="/assets/index-D5RkA1Qc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BcWuvM_a.css">
|
||||
<script type="module" crossorigin src="/assets/index-DyqIjjBz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-o3d5k2lQ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Users, BookOpen, ShoppingBag, TrendingUp, RefreshCw, ChevronRight } from 'lucide-react'
|
||||
import { Users, BookOpen, ShoppingBag, TrendingUp, RefreshCw, ChevronRight, BarChart3 } from 'lucide-react'
|
||||
import { get } from '@/api/client'
|
||||
import { UserDetailModal } from '@/components/modules/user/UserDetailModal'
|
||||
|
||||
@@ -75,6 +75,14 @@ export function DashboardPage() {
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
const [detailUserId, setDetailUserId] = useState<string | null>(null)
|
||||
const [showDetailModal, setShowDetailModal] = useState(false)
|
||||
const [giftedTotal, setGiftedTotal] = useState(0)
|
||||
const [ordersExpanded, setOrdersExpanded] = useState(false)
|
||||
const [trackPeriod, setTrackPeriod] = useState<string>('week')
|
||||
const [trackStats, setTrackStats] = useState<{
|
||||
total: number
|
||||
byModule: Record<string, { action: string; target: string; module: string; page: string; count: number }[]>
|
||||
} | null>(null)
|
||||
const [trackLoading, setTrackLoading] = useState(false)
|
||||
|
||||
const showError = (err: unknown) => {
|
||||
const e = err as Error & { status?: number; name?: string }
|
||||
@@ -116,13 +124,23 @@ export function DashboardPage() {
|
||||
setStatsLoading(false)
|
||||
}
|
||||
|
||||
// 加载代付总额(仅用于收入标签展示)
|
||||
try {
|
||||
const balRes = await get<{ success?: boolean; data?: { totalGifted?: number } }>('/api/admin/balance/summary', init)
|
||||
if (balRes?.success && balRes.data) {
|
||||
setGiftedTotal(balRes.data.totalGifted ?? 0)
|
||||
}
|
||||
} catch {
|
||||
// 不影响主面板
|
||||
}
|
||||
|
||||
// 2. 并行加载订单和用户
|
||||
setOrdersLoading(true)
|
||||
setUsersLoading(true)
|
||||
const loadOrders = async () => {
|
||||
try {
|
||||
const res = await get<{ success?: boolean; recentOrders?: OrderRow[] }>(
|
||||
'/api/admin/dashboard/recent-orders',
|
||||
'/api/admin/dashboard/recent-orders?limit=10',
|
||||
init
|
||||
)
|
||||
if (res?.success && res.recentOrders) setPurchases(res.recentOrders)
|
||||
@@ -166,10 +184,28 @@ export function DashboardPage() {
|
||||
await Promise.all([loadOrders(), loadUsers()])
|
||||
}
|
||||
|
||||
async function loadTrackStats(period?: string) {
|
||||
const p = period || trackPeriod
|
||||
setTrackLoading(true)
|
||||
try {
|
||||
const res = await get<{ success?: boolean; total?: number; byModule?: Record<string, { action: string; target: string; module: string; page: string; count: number }[]> }>(
|
||||
`/api/admin/track/stats?period=${p}`
|
||||
)
|
||||
if (res?.success) {
|
||||
setTrackStats({ total: res.total ?? 0, byModule: res.byModule ?? {} })
|
||||
}
|
||||
} catch {
|
||||
setTrackStats(null)
|
||||
} finally {
|
||||
setTrackLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const ctrl = new AbortController()
|
||||
loadAll(ctrl.signal)
|
||||
const timer = setInterval(() => loadAll(), 30000)
|
||||
loadTrackStats()
|
||||
const timer = setInterval(() => { loadAll(); loadTrackStats() }, 30000)
|
||||
return () => {
|
||||
ctrl.abort()
|
||||
clearInterval(timer)
|
||||
@@ -185,6 +221,10 @@ export function DashboardPage() {
|
||||
const amount = typeof p.amount === 'number' ? p.amount.toFixed(2) : parseFloat(String(p.amount || '0')).toFixed(2)
|
||||
return { title: `余额充值 ¥${amount}`, subtitle: '余额充值' }
|
||||
}
|
||||
if (type === 'gift_pay') {
|
||||
const amount = typeof p.amount === 'number' ? p.amount.toFixed(2) : parseFloat(String(p.amount || '0')).toFixed(2)
|
||||
return { title: `代付 ¥${amount}`, subtitle: '好友代付' }
|
||||
}
|
||||
if (desc) {
|
||||
if (type === 'section' && desc.includes('章节')) {
|
||||
if (desc.includes('-')) {
|
||||
@@ -223,6 +263,7 @@ export function DashboardPage() {
|
||||
{
|
||||
title: '总用户数',
|
||||
value: statsLoading ? null : totalUsers,
|
||||
sub: null as string | null,
|
||||
icon: Users,
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-500/20',
|
||||
@@ -231,6 +272,7 @@ export function DashboardPage() {
|
||||
{
|
||||
title: '总收入',
|
||||
value: statsLoading ? null : `¥${(totalRevenue ?? 0).toFixed(2)}`,
|
||||
sub: giftedTotal > 0 ? `含代付 ¥${giftedTotal.toFixed(2)}` : null,
|
||||
icon: TrendingUp,
|
||||
color: 'text-[#38bdac]',
|
||||
bg: 'bg-[#38bdac]/20',
|
||||
@@ -239,6 +281,7 @@ export function DashboardPage() {
|
||||
{
|
||||
title: '订单数',
|
||||
value: statsLoading ? null : paidOrderCount,
|
||||
sub: null as string | null,
|
||||
icon: ShoppingBag,
|
||||
color: 'text-purple-400',
|
||||
bg: 'bg-purple-500/20',
|
||||
@@ -247,6 +290,7 @@ export function DashboardPage() {
|
||||
{
|
||||
title: '转化率',
|
||||
value: statsLoading ? null : `${typeof conversionRate === 'number' ? conversionRate.toFixed(1) : 0}%`,
|
||||
sub: null as string | null,
|
||||
icon: BookOpen,
|
||||
color: 'text-orange-400',
|
||||
bg: 'bg-orange-500/20',
|
||||
@@ -284,14 +328,19 @@ export function DashboardPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-2xl font-bold text-white min-h-[2rem] flex items-center">
|
||||
{stat.value != null ? (
|
||||
stat.value
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-2 text-gray-500">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
加载中
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-white min-h-8 flex items-center">
|
||||
{stat.value != null ? (
|
||||
stat.value
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-2 text-gray-500">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
加载中
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{stat.sub && (
|
||||
<p className="text-xs text-gray-500 mt-1">{stat.sub}</p>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-600 group-hover:text-[#38bdac] transition-colors" />
|
||||
@@ -330,7 +379,7 @@ export function DashboardPage() {
|
||||
) : (
|
||||
<>
|
||||
{purchases
|
||||
.slice(0, 5)
|
||||
.slice(0, ordersExpanded ? 10 : 4)
|
||||
.map((p) => {
|
||||
const referrer: UserRow | undefined = p.referrerId
|
||||
? users.find((u) => u.id === p.referrerId)
|
||||
@@ -416,6 +465,15 @@ export function DashboardPage() {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{purchases.length > 4 && !ordersExpanded && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOrdersExpanded(true)}
|
||||
className="w-full py-2 text-sm text-[#38bdac] hover:text-[#2da396] border border-dashed border-gray-600 rounded-lg hover:border-[#38bdac]/50 transition-colors"
|
||||
>
|
||||
展开更多
|
||||
</button>
|
||||
)}
|
||||
{purchases.length === 0 && !ordersLoading && (
|
||||
<div className="text-center py-12">
|
||||
<ShoppingBag className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
@@ -480,6 +538,114 @@ export function DashboardPage() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="mt-8 bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5 text-[#38bdac]" />
|
||||
分类标签点击统计
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{(['today', 'week', 'month', 'all'] as const).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
onClick={() => { setTrackPeriod(p); loadTrackStats(p) }}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
trackPeriod === p
|
||||
? 'bg-[#38bdac] text-white'
|
||||
: 'bg-gray-700/50 text-gray-400 hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{{ today: '今日', week: '本周', month: '本月', all: '全部' }[p]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{trackLoading && !trackStats ? (
|
||||
<div className="flex items-center justify-center py-12 text-gray-500">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-2" />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
) : trackStats && Object.keys(trackStats.byModule).length > 0 ? (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-gray-400">
|
||||
总点击 <span className="text-white font-bold text-lg">{trackStats.total}</span> 次
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Object.entries(trackStats.byModule)
|
||||
.sort((a, b) => b[1].reduce((s, i) => s + i.count, 0) - a[1].reduce((s, i) => s + i.count, 0))
|
||||
.map(([mod, items]) => {
|
||||
const moduleTotal = items.reduce((s, i) => s + i.count, 0)
|
||||
const moduleLabels: Record<string, string> = {
|
||||
home: '首页', chapters: '目录', read: '阅读', my: '我的',
|
||||
vip: 'VIP', wallet: '钱包', match: '找伙伴', referral: '推广',
|
||||
search: '搜索', settings: '设置', about: '关于', other: '其他',
|
||||
}
|
||||
return (
|
||||
<div key={mod} className="bg-[#0a1628] rounded-lg border border-gray-700/30 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-[#38bdac]">
|
||||
{moduleLabels[mod] || mod}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{moduleTotal} 次</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{items
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 8)
|
||||
.map((item, i) => {
|
||||
const targetLabels: Record<string, string> = {
|
||||
'开始匹配': '开始匹配', 'mentor': '导师顾问', 'team': '团队招募',
|
||||
'investor': '资源对接', '充值': '充值', '退款': '退款',
|
||||
'wallet': '钱包', '设置': '设置', 'VIP': 'VIP会员',
|
||||
'推广': '推广中心', '目录': '目录', '搜索': '搜索',
|
||||
'匹配': '找伙伴', 'settings': '设置', 'expired': '已过期',
|
||||
'active': '活跃', 'converted': '已转化', 'fill_profile': '完善资料',
|
||||
'register': '注册', 'purchase': '购买', 'btn_click': '按钮点击',
|
||||
'nav_click': '导航点击', 'card_click': '卡片点击', 'tab_click': '标签切换',
|
||||
'rule_trigger': '规则触发', 'view_chapter': '浏览章节',
|
||||
'链接卡若': '链接卡若', '更多分享': '更多分享', '分享朋友圈文案': '分享朋友圈',
|
||||
'选择金额10': '选择金额10元',
|
||||
}
|
||||
const actionLabels: Record<string, string> = {
|
||||
'btn_click': '按钮点击', 'nav_click': '导航点击', 'card_click': '卡片点击',
|
||||
'tab_click': '标签切换', 'purchase': '购买', 'register': '注册',
|
||||
'rule_trigger': '规则触发', 'view_chapter': '浏览章节',
|
||||
}
|
||||
const label = targetLabels[item.target] || item.target || actionLabels[item.action] || item.action
|
||||
return (
|
||||
<div key={i} className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-300 truncate mr-2" title={`${item.action}: ${item.target}`}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="w-16 h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#38bdac] rounded-full"
|
||||
style={{ width: `${moduleTotal > 0 ? (item.count / moduleTotal) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-gray-400 w-8 text-right">{item.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<BarChart3 className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500">暂无点击数据</p>
|
||||
<p className="text-gray-600 text-xs mt-1">小程序端接入埋点后,数据将在此实时展示</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<UserDetailModal
|
||||
open={showDetailModal}
|
||||
onClose={() => { setShowDetailModal(false); setDetailUserId(null) }}
|
||||
|
||||
@@ -63,6 +63,7 @@ interface FeatureConfig {
|
||||
matchEnabled: boolean
|
||||
referralEnabled: boolean
|
||||
searchEnabled: boolean
|
||||
aboutEnabled: boolean
|
||||
}
|
||||
|
||||
interface MpConfig {
|
||||
@@ -70,6 +71,7 @@ interface MpConfig {
|
||||
withdrawSubscribeTmplId?: string
|
||||
mchId?: string
|
||||
minWithdraw?: number
|
||||
auditMode?: boolean
|
||||
}
|
||||
|
||||
interface OssConfig {
|
||||
@@ -108,6 +110,7 @@ const defaultFeatures: FeatureConfig = {
|
||||
matchEnabled: true,
|
||||
referralEnabled: true,
|
||||
searchEnabled: true,
|
||||
aboutEnabled: true,
|
||||
}
|
||||
|
||||
const TAB_KEYS = ['system', 'author', 'admin', 'api-docs'] as const
|
||||
@@ -205,6 +208,30 @@ export function SettingsPage() {
|
||||
saveFeatureConfigOnly(next, () => setFeatureConfig(prev))
|
||||
}
|
||||
|
||||
const [auditModeSaving, setAuditModeSaving] = useState(false)
|
||||
const handleAuditModeSwitch = async (checked: boolean) => {
|
||||
const prev = mpConfig
|
||||
const next = { ...prev, auditMode: checked }
|
||||
setMpConfig(next)
|
||||
setAuditModeSaving(true)
|
||||
try {
|
||||
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
|
||||
mpConfig: next,
|
||||
})
|
||||
if (!res || (res as { success?: boolean }).success === false) {
|
||||
setMpConfig(prev)
|
||||
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
|
||||
return
|
||||
}
|
||||
showResult('已保存', checked ? '审核模式已开启,小程序将隐藏所有支付入口。' : '审核模式已关闭,支付功能已恢复。')
|
||||
} catch (error) {
|
||||
setMpConfig(prev)
|
||||
showResult('保存失败', error instanceof Error ? error.message : String(error), true)
|
||||
} finally {
|
||||
setAuditModeSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
@@ -223,6 +250,7 @@ export function SettingsPage() {
|
||||
withdrawSubscribeTmplId: mpConfig.withdrawSubscribeTmplId || '',
|
||||
mchId: mpConfig.mchId || '',
|
||||
minWithdraw: typeof mpConfig.minWithdraw === 'number' ? mpConfig.minWithdraw : 10,
|
||||
auditMode: mpConfig.auditMode ?? false,
|
||||
},
|
||||
ossConfig: Object.keys(ossConfig).length
|
||||
? {
|
||||
@@ -570,78 +598,6 @@ export function SettingsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Settings className="w-5 h-5 text-[#38bdac]" />
|
||||
功能开关
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
控制各个功能模块的显示/隐藏
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="match-enabled" className="text-white font-medium cursor-pointer">
|
||||
找伙伴功能
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制小程序和Web端的找伙伴功能显示</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="match-enabled"
|
||||
checked={featureConfig.matchEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('matchEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gift className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="referral-enabled" className="text-white font-medium cursor-pointer">
|
||||
推广功能
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制推广中心的显示(我的页面入口)</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="referral-enabled"
|
||||
checked={featureConfig.referralEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('referralEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="search-enabled" className="text-white font-medium cursor-pointer">
|
||||
搜索功能
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制首页、目录页搜索栏的显示</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="search-enabled"
|
||||
checked={featureConfig.searchEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('searchEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/30">
|
||||
<p className="text-xs text-blue-300">
|
||||
💡 关闭功能后,相关入口会自动隐藏。建议在功能开发完成后再开启。
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
@@ -713,6 +669,130 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={`bg-[#0f2137] shadow-xl ${mpConfig.auditMode ? 'border-amber-500/50 border-2' : 'border-gray-700/50'}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-amber-400" />
|
||||
小程序审核模式
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
提交微信审核前开启,审核通过后关闭即可恢复支付功能
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`flex items-center justify-between p-4 rounded-lg border ${mpConfig.auditMode ? 'bg-amber-500/10 border-amber-500/30' : 'bg-[#0a1628] border-gray-700/50'}`}>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className={`w-4 h-4 ${mpConfig.auditMode ? 'text-amber-400' : 'text-gray-400'}`} />
|
||||
<Label htmlFor="audit-mode" className="text-white font-medium cursor-pointer">
|
||||
{mpConfig.auditMode ? '审核模式(已开启)' : '审核模式(已关闭)'}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">
|
||||
{mpConfig.auditMode
|
||||
? '当前已隐藏所有支付、VIP、充值、收益等入口,审核员看不到任何付费内容'
|
||||
: '关闭状态,小程序正常显示所有功能(含支付、VIP 等)'}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="audit-mode"
|
||||
checked={mpConfig.auditMode ?? false}
|
||||
disabled={auditModeSaving}
|
||||
onCheckedChange={handleAuditModeSwitch}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Settings className="w-5 h-5 text-[#38bdac]" />
|
||||
功能开关
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
控制各个功能模块的显示/隐藏
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="match-enabled" className="text-white font-medium cursor-pointer">
|
||||
找伙伴功能
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制小程序和Web端的找伙伴功能显示</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="match-enabled"
|
||||
checked={featureConfig.matchEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('matchEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gift className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="referral-enabled" className="text-white font-medium cursor-pointer">
|
||||
推广功能
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制推广中心的显示(我的页面入口)</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="referral-enabled"
|
||||
checked={featureConfig.referralEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('referralEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="search-enabled" className="text-white font-medium cursor-pointer">
|
||||
搜索功能
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制首页、目录页搜索栏的显示</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="search-enabled"
|
||||
checked={featureConfig.searchEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('searchEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="about-enabled" className="text-white font-medium cursor-pointer">
|
||||
关于页面
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制关于页面的访问</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="about-enabled"
|
||||
checked={featureConfig.aboutEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('aboutEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/30">
|
||||
<p className="text-xs text-blue-300">
|
||||
💡 关闭功能后,相关入口会自动隐藏。建议在功能开发完成后再开启。
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -236,9 +236,17 @@ export function UsersPage() {
|
||||
if (!confirm('确定要删除这个用户吗?')) return
|
||||
try {
|
||||
const data = await del<{ success?: boolean; error?: string }>(`/api/db/users?id=${encodeURIComponent(userId)}`)
|
||||
if (data?.success) loadUsers()
|
||||
else toast.error('删除失败: ' + (data?.error || ''))
|
||||
} catch { toast.error('删除失败') }
|
||||
if (data?.success) {
|
||||
toast.success('已删除')
|
||||
loadUsers()
|
||||
} else {
|
||||
toast.error('删除失败: ' + (data?.error || '未知错误'))
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error & { data?: { error?: string } }
|
||||
const msg = err?.data?.error || err?.message || '网络错误'
|
||||
toast.error('删除失败: ' + msg)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditUser = (user: User) => {
|
||||
|
||||
@@ -3,7 +3,9 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
@@ -54,11 +56,17 @@ func AdminDashboardStats(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// AdminDashboardRecentOrders GET /api/admin/dashboard/recent-orders
|
||||
// AdminDashboardRecentOrders GET /api/admin/dashboard/recent-orders?limit=10
|
||||
func AdminDashboardRecentOrders(c *gin.Context) {
|
||||
db := database.DB()
|
||||
limit := 5
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if n, err := strconv.Atoi(l); err == nil && n >= 1 && n <= 20 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
var recentOrders []model.Order
|
||||
db.Where("status IN ?", paidStatuses).Order("created_at DESC").Limit(5).Find(&recentOrders)
|
||||
db.Where("status IN ?", paidStatuses).Order("created_at DESC").Limit(limit).Find(&recentOrders)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "recentOrders": buildRecentOrdersOut(db, recentOrders)})
|
||||
}
|
||||
|
||||
@@ -180,6 +188,101 @@ func buildRecentOrdersOut(db *gorm.DB, recentOrders []model.Order) []gin.H {
|
||||
return out
|
||||
}
|
||||
|
||||
// AdminTrackStats GET /api/admin/track/stats?period=today|week|month|all
|
||||
// 埋点统计:按 extra_data->module 分组,按 action+target 聚合 count
|
||||
func AdminTrackStats(c *gin.Context) {
|
||||
period := c.DefaultQuery("period", "week")
|
||||
if period != "today" && period != "week" && period != "month" && period != "all" {
|
||||
period = "week"
|
||||
}
|
||||
now := time.Now()
|
||||
var start time.Time
|
||||
switch period {
|
||||
case "today":
|
||||
start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
case "week":
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
start = time.Date(now.Year(), now.Month(), now.Day()-weekday+1, 0, 0, 0, 0, now.Location())
|
||||
case "month":
|
||||
start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
case "all":
|
||||
start = time.Time{}
|
||||
}
|
||||
db := database.DB()
|
||||
var tracks []model.UserTrack
|
||||
q := db.Model(&model.UserTrack{})
|
||||
if !start.IsZero() {
|
||||
q = q.Where("created_at >= ?", start)
|
||||
}
|
||||
if err := q.Find(&tracks).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
// byModule: module -> map[key] -> count, key = action + "|" + target
|
||||
type item struct {
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Module string `json:"module"`
|
||||
Page string `json:"page"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
byModule := make(map[string]map[string]*item)
|
||||
total := 0
|
||||
for _, t := range tracks {
|
||||
total++
|
||||
module := "other"
|
||||
page := ""
|
||||
if len(t.ExtraData) > 0 {
|
||||
var extra map[string]interface{}
|
||||
if err := json.Unmarshal(t.ExtraData, &extra); err == nil {
|
||||
if m, ok := extra["module"].(string); ok && m != "" {
|
||||
module = m
|
||||
}
|
||||
if p, ok := extra["page"].(string); ok {
|
||||
page = p
|
||||
}
|
||||
}
|
||||
}
|
||||
target := ""
|
||||
if t.Target != nil {
|
||||
target = *t.Target
|
||||
}
|
||||
key := t.Action + "|" + target
|
||||
if byModule[module] == nil {
|
||||
byModule[module] = make(map[string]*item)
|
||||
}
|
||||
if byModule[module][key] == nil {
|
||||
byModule[module][key] = &item{Action: t.Action, Target: target, Module: module, Page: page, Count: 0}
|
||||
}
|
||||
byModule[module][key].Count++
|
||||
}
|
||||
// 转为前端期望格式:byModule[module] = [{action,target,module,page,count},...]
|
||||
out := make(map[string][]gin.H)
|
||||
for mod, m := range byModule {
|
||||
list := make([]gin.H, 0, len(m))
|
||||
for _, v := range m {
|
||||
list = append(list, gin.H{
|
||||
"action": v.Action, "target": v.Target, "module": v.Module, "page": v.Page, "count": v.Count,
|
||||
})
|
||||
}
|
||||
out[mod] = list
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "total": total, "byModule": out})
|
||||
}
|
||||
|
||||
// AdminBalanceSummary GET /api/admin/balance/summary
|
||||
// 汇总代付金额(product_type=gift_pay 的已支付订单金额),用于 Dashboard 显示「含代付 ¥xx」
|
||||
func AdminBalanceSummary(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var totalGifted float64
|
||||
db.Model(&model.Order{}).Where("product_type = ? AND status IN ?", "gift_pay", paidStatuses).
|
||||
Select("COALESCE(SUM(amount), 0)").Scan(&totalGifted)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalGifted": totalGifted}})
|
||||
}
|
||||
|
||||
// AdminDashboardMerchantBalance GET /api/admin/dashboard/merchant-balance
|
||||
// 查询微信商户号实时余额(可用余额、待结算余额),用于看板展示
|
||||
// 注意:普通商户可能需向微信申请开通权限,未开通时返回 error
|
||||
|
||||
@@ -39,6 +39,8 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
"minWithdraw": 10,
|
||||
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
|
||||
"mchId": "1318592501",
|
||||
"auditMode": false,
|
||||
"supportWechat": true,
|
||||
}
|
||||
|
||||
out := gin.H{
|
||||
@@ -134,6 +136,14 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
if _, has := out["linkedMiniprograms"]; !has {
|
||||
out["linkedMiniprograms"] = []gin.H{}
|
||||
}
|
||||
// 明确归一化 auditMode:仅当 DB 显式为 true 时返回 true,否则一律 false(避免历史脏数据/类型异常导致误判)
|
||||
if mp, ok := out["mpConfig"].(gin.H); ok {
|
||||
if v, ok := mp["auditMode"].(bool); ok && v {
|
||||
mp["auditMode"] = true
|
||||
} else {
|
||||
mp["auditMode"] = false
|
||||
}
|
||||
}
|
||||
cache.Set(context.Background(), cache.KeyConfigMiniprogram, out, cache.ConfigTTL)
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
@@ -179,10 +189,12 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
|
||||
"mchId": "1318592501",
|
||||
"minWithdraw": float64(10),
|
||||
"auditMode": false,
|
||||
"supportWechat": true,
|
||||
}
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true},
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true},
|
||||
"siteSettings": gin.H{"sectionPrice": float64(1), "baseBookPrice": 9.9, "distributorShare": float64(90), "authorInfo": gin.H{}},
|
||||
"mpConfig": defaultMp,
|
||||
"ossConfig": gin.H{},
|
||||
@@ -902,18 +914,24 @@ func randomSuffix() string {
|
||||
return fmt.Sprintf("%d%x", time.Now().UnixNano()%100000, time.Now().UnixNano()&0xfff)
|
||||
}
|
||||
|
||||
// DBUsersDelete DELETE /api/db/users
|
||||
// DBUsersDelete DELETE /api/db/users(软删除:仅设置 deleted_at,用户再次登录会新建账号)
|
||||
func DBUsersDelete(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户ID不能为空"})
|
||||
return
|
||||
}
|
||||
if err := database.DB().Where("id = ?", id).Delete(&model.User{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
db := database.DB()
|
||||
result := db.Where("id = ?", id).Delete(&model.User{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户删除成功"})
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在或已被删除"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户已删除(假删除),该用户再次登录将创建新账号"})
|
||||
}
|
||||
|
||||
// DBUsersReferrals GET /api/db/users/referrals(绑定关系详情弹窗;收益与「已付费」与小程序口径一致:订单+提现表实时计算)
|
||||
|
||||
@@ -69,8 +69,8 @@ func MiniprogramLogin(c *gin.Context) {
|
||||
isNewUser := result.Error != nil
|
||||
|
||||
if isNewUser {
|
||||
// 创建新用户
|
||||
userID := openID // 直接使用 openid 作为用户 ID
|
||||
// 创建新用户(含软删除后再次登录:旧记录 id=openid 仍存在,需用新 id 避免主键冲突)
|
||||
userID := "user_" + randomSuffix()
|
||||
referralCode := "SOUL" + strings.ToUpper(openID[len(openID)-6:])
|
||||
nickname := "微信用户" + openID[len(openID)-4:]
|
||||
avatar := ""
|
||||
@@ -393,9 +393,17 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
clientIP = "127.0.0.1"
|
||||
}
|
||||
|
||||
// userID:优先用客户端传入;为空时按 openid 查用户(排除软删除,避免订单归属到旧账号)
|
||||
userID := req.UserID
|
||||
if userID == "" {
|
||||
userID = req.OpenID
|
||||
if userID == "" && req.OpenID != "" {
|
||||
var u model.User
|
||||
if err := db.Where("open_id = ?", req.OpenID).First(&u).Error; err == nil {
|
||||
userID = u.ID
|
||||
} else {
|
||||
// 查不到用户:可能是未登录或软删除后未重新登录,避免用 openid 导致订单归属到旧账号
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请先登录后再支付"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
productID := req.ProductID
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -658,6 +659,11 @@ func MiniprogramTrackPost(c *gin.Context) {
|
||||
if body.Target != "" {
|
||||
t.ChapterID = &chID
|
||||
}
|
||||
if body.ExtraData != nil {
|
||||
if b, err := json.Marshal(body.ExtraData); err == nil {
|
||||
t.ExtraData = b
|
||||
}
|
||||
}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -54,6 +54,8 @@ func WechatPhoneLogin(c *gin.Context) {
|
||||
isNewUser := result.Error != nil
|
||||
|
||||
if isNewUser {
|
||||
// 软删除后再次登录:旧记录 id=openid 仍存在,需用新 id 避免主键冲突
|
||||
userID := "user_" + randomSuffix()
|
||||
referralCode := "SOUL" + strings.ToUpper(openID[len(openID)-6:])
|
||||
nickname := "微信用户" + openID[len(openID)-4:]
|
||||
avatar := ""
|
||||
@@ -67,7 +69,7 @@ func WechatPhoneLogin(c *gin.Context) {
|
||||
phone = "+" + countryCode + " " + phoneNumber
|
||||
}
|
||||
user = model.User{
|
||||
ID: openID,
|
||||
ID: userID,
|
||||
OpenID: &openID,
|
||||
SessionKey: &sessionKey,
|
||||
Nickname: &nickname,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 对应表 users,JSON 输出与现网接口 1:1(小写驼峰)
|
||||
// 软删除:管理端删除仅设置 deleted_at,用户再次登录会创建新账号
|
||||
type User struct {
|
||||
ID string `gorm:"column:id;primaryKey;size:50" json:"id"`
|
||||
OpenID *string `gorm:"column:open_id;size:100" json:"openId,omitempty"`
|
||||
@@ -50,6 +55,9 @@ type User struct {
|
||||
VipContact *string `gorm:"column:vip_contact;size:100" json:"vipContact,omitempty"`
|
||||
VipBio *string `gorm:"column:vip_bio;type:text" json:"vipBio,omitempty"`
|
||||
|
||||
// 软删除:管理端假删除,用户再次登录会新建账号
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
|
||||
|
||||
// 以下为接口返回时从订单/绑定表实时计算的字段,不入库
|
||||
PurchasedSectionCount int `gorm:"-" json:"purchasedSectionCount,omitempty"`
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
admin.PUT("/orders/refund", handler.AdminOrderRefund)
|
||||
admin.GET("/users/:id/balance", handler.AdminUserBalanceGet)
|
||||
admin.POST("/users/:id/balance/adjust", handler.AdminUserBalanceAdjust)
|
||||
admin.GET("/balance/summary", handler.AdminBalanceSummary)
|
||||
admin.GET("/users", handler.AdminUsersList)
|
||||
admin.POST("/users", handler.AdminUsersAction)
|
||||
admin.PUT("/users", handler.AdminUsersAction)
|
||||
@@ -100,6 +101,7 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
admin.GET("/orders", handler.OrdersList)
|
||||
admin.GET("/gift-pay-requests", handler.AdminGiftPayRequestsList)
|
||||
admin.GET("/user/track", handler.UserTrackGet)
|
||||
admin.GET("/track/stats", handler.AdminTrackStats)
|
||||
}
|
||||
|
||||
// ----- 鉴权 -----
|
||||
|
||||
5
soul-api/scripts/add-users-soft-delete.sql
Normal file
5
soul-api/scripts/add-users-soft-delete.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- 用户软删除:管理端假删除,用户再次登录会新建账号
|
||||
-- 执行后,DELETE 操作改为 SET deleted_at,不再物理删除,避免外键约束
|
||||
|
||||
ALTER TABLE users ADD COLUMN deleted_at DATETIME(3) NULL DEFAULT NULL COMMENT '软删除时间' AFTER updated_at;
|
||||
CREATE INDEX idx_users_deleted_at ON users (deleted_at);
|
||||
@@ -37411,3 +37411,13 @@
|
||||
{"level":"debug","timestamp":"2026-03-17T15:11:49+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 52\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Tue, 17 Mar 2026 07:11:49 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08B5FDE3CD0610B50418D8AAC05520C4ED0628CC8C03-0\r\nServer: nginx\r\nWechatpay-Nonce: 3c7de243555486019ee5407a5befc17e\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: E6BMV0sFgHY9cq/I15VUAgmiZJty5KXHl9aSVPEheYnbUcve6J9VN4bqLkYQt2lq6iilE1AifBmGxBjek/XAdixo2RbJ1yiCr9yM+Yxdi3RrgHXYMYURq9QK8gflbIH5GZS7eg+kYKueYPaAIRq1LHAD3aCfnzcD0fq49GlMGOi7bVQw1v6wW3cNy94cJZ5e32VsFmcXXjybe7CxYnLSo4s5kOsdjSRQJiJOSazplDyKHgeqHpflqFO6SkJvROAWOTFJQVYXgylR6Vr/ZEPDPZntiMGqRYpblFoY8wakewOGg+qZKzBW7DUTkmQUK7B8564oD6+E+mebdC0f8HhW3w==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773731509\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"prepay_id\":\"wx17151149756818d4257dcb4e2b83250001\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-17T15:17:26+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260317151148855100?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"iXHM3yIdYUhXvQC03FIKemHVDccXRE6r\",timestamp=\"1773731845\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"2KL23qFk/Gs8t0qsYz2Xuy5FemFByzUW5uvS39TBcRn6ziT1ANz1T2U2ZxUuiAXRt99ewXC8Pnsu/XZSf6UCP1vqwFFIcrZ3Wz+1FHjdvhPUL8ZIqD5ymCwRZANoZtaPSd3h7C8f4JT+ET+Inn16v7ezyUDY7TdaFCVvhhCy84/NDyvGIw41i2cF+jA4wySEJ15Lg0Tp7sgOglhYdR8tpngN2qIjUsCl1yToDAU3MvKUp3DhKh5HnMGAdSuENCyFEdL0RYU5mqbggtFnlV0WODsORJ+MKrbfvuA4Q0zVZZnF16OWwP2pfJ32RF6U4eFDb07ICI0hrVgKaHq+AAnOCw==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-17T15:17:26+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 249\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Tue, 17 Mar 2026 07:17:26 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 088680E4CD0610FB0218DD9C85AB0120F8DE0528F19905-0\r\nServer: nginx\r\nWechatpay-Nonce: 1179b7f5d9c0211c6769de56735f874f\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: VrORKkCkmK8WqBCTWhl0w9Y5rKzRfoDATiKJASSb6qwsctuJqhBdmRZ8IhAyAxM0t7hDCQOvxRxbklVYYlGsYk8Ozsf2VvoH3dBkXlMO8TYMKEkuB/dgQ6VO2fKswvG72dIv6bXfhU+GE8Eu6ENiAfD5eGTjjQAPbFXiJTctcvhggta/vKTPBBnsI47bFWeHzKSVMzkyE+uXpuo97o++TDu/s8C6l+Z+mG/01LE2k4OfvHkXifpARlIyDZr3MQFCGacDniWDr0vqj6Zlaf5PwIZhjEhjcpWbLoSHMSfb6Pt00wunMZLS20m+t/GKYJ3Qz9z3fC+A0Z1j/uS7UJlGVQ==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773731846\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":100},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260317151148855100\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-17T15:42:50+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260317151148855100?mchid=1318592501 request header: { Authorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"k2Ec5z8tXyopDUfxB1ShfFtpTdUsYK79\",timestamp=\"1773733369\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"MvS1MBSKDn91cAA99yEzIBO0d/lLdn+y4yyyNcRmoF6MZpHvmQYoZDgIOh/P3MVOJljPBd3sh+YNNQGgTPUmEG6B62Cz1QfsuYIH21dMnc/AMeHTJy0AZy1tpaerPfdT8nCK00l/wWcYl/Yr41of9U1rKgWAjv5xtPju3vVdBKwUD06klN6PsyttdNsMd7pq3tIp6bUx9+84WOX/a5W3/fm1kXTUlH5vFHQ6RB698zgp4O2QbAlEy2aFla1RJwfc+BXVWn0pMm5Ko9esFnGF8juiO7DkNcBkvF3sOpDOS8UrubMI/Q36sjstTLzws5vfZ9eITB+3QqAZJLcvpMZQGA==\"Accept:*/*Content-Type:application/json} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-17T15:42:50+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 249\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Tue, 17 Mar 2026 07:42:50 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08FA8BE4CD06108E0418FEC1C055208EFA0428D5E002-0\r\nServer: nginx\r\nWechatpay-Nonce: a0d4c4e850561fc58f5272cd45e4ecc5\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: t7nSQEDzhKrd6LKnoQveSP7DWn3sCWme5tx+X5lZC2EziBEjekQHwc1J0TpFjGd+4mZYYMEpFBeHOKMAwkLOOqIXxGqB5H1Pe4orLmW/lwqF6V4uLOKtmFdNszDHEWZQyUykLw9fBegpegF2k9iNmf3oWUHaIobdti0QD2d16WeiVWzEg6EVhDYQZxxrTpTdpDWLl85tqgIKv1OGQ3I4qnPfHMsB+D3/CelmEeemSwn9otBS825CjSj8hXdYcI2MKlmdMLcm/ZO/gOJI3SM5AV3nobqE6yCCbq0mcwUiIjoLM/gFYqgM9rqDrEdiFcbF7LVxV/9jtM1YAeVpXmEhyg==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773733370\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":100},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260317151148855100\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-17T16:33:25+08:00","caller":"kernel/accessToken.go:381","content":"GET https://api.weixin.qq.com/cgi-bin/token?appid=wxb8bbb2b10dec74aa&grant_type=client_credential&neededText=&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-17T16:33:25+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Tue, 17 Mar 2026 08:33:25 GMT\r\n\r\n{\"access_token\":\"102_x2PDa5E3z33zzL-P19BDZ685W7ItA8HHxiSgq_oz9yy9BAs5cEvsDL7m4pInyC7LhIpnUQ3Sa_2_DCdpzMF0VxQnuNM___aJlchBAXpa5EGnF2VfUI1GypVxvOkNNAcAAAFFS\",\"expires_in\":7200}"}
|
||||
{"level":"debug","timestamp":"2026-03-17T16:33:25+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_x2PDa5E3z33zzL-P19BDZ685W7ItA8HHxiSgq_oz9yy9BAs5cEvsDL7m4pInyC7LhIpnUQ3Sa_2_DCdpzMF0VxQnuNM___aJlchBAXpa5EGnF2VfUI1GypVxvOkNNAcAAAFFS&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0e33HYFa1EKLnL0I1RGa1EvoK113HYFW&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-17T16:33:25+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Tue, 17 Mar 2026 08:33:26 GMT\r\n\r\n{\"session_key\":\"pjoOisYFMOpimtJsFYj8yA==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-17T17:40:08+08:00","caller":"kernel/accessToken.go:381","content":"GET https://api.weixin.qq.com/cgi-bin/token?appid=wxb8bbb2b10dec74aa&grant_type=client_credential&neededText=&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-17T17:40:08+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Tue, 17 Mar 2026 09:40:08 GMT\r\n\r\n{\"access_token\":\"102_qKu7EbmqaaPGhTC2LXZkSg0zAoyqTVs6EHwGSES_0Qj1oSKagRwZNq91KfcsJIeEIQbrH3vzUBBAnCOTZKPMTQkpa8TTl6PqO7kBOAPT8A0Wt1MaIwnD3NEibzYHNMbAFAWCB\",\"expires_in\":7200}"}
|
||||
{"level":"debug","timestamp":"2026-03-17T17:40:08+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_qKu7EbmqaaPGhTC2LXZkSg0zAoyqTVs6EHwGSES_0Qj1oSKagRwZNq91KfcsJIeEIQbrH3vzUBBAnCOTZKPMTQkpa8TTl6PqO7kBOAPT8A0Wt1MaIwnD3NEibzYHNMbAFAWCB&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0b3L3mll2tGInh4UuUll2YDmWU0L3mlq&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-17T17:40:08+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Tue, 17 Mar 2026 09:40:09 GMT\r\n\r\n{\"session_key\":\"BvvCTmjR14OphaYQdgJ9Ow==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
|
||||
185
开发文档/新版迁移-功能差异清单.md
Normal file
185
开发文档/新版迁移-功能差异清单.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 新版迁移 - 小程序功能差异清单(稳定版 vs 体验版)
|
||||
|
||||
> 乘风分析:稳定版(miniprogram)与体验版(new-soul/miniprogram)的差异。
|
||||
> 更新日期:2026-03-17
|
||||
|
||||
**目录说明**:`miniprogram` = 稳定版(生产);`new-soul/miniprogram` = 体验版(新版参考)
|
||||
|
||||
---
|
||||
|
||||
## 一、差异摘要(按页面/模块)
|
||||
|
||||
| 页面/模块 | 稳定版 miniprogram | 体验版 new-soul/miniprogram | 迁移建议 |
|
||||
|-----------|-------------------|-----------------------------|----------|
|
||||
| **app.js** | 硬编码 baseUrl、无 auditMode | loadRuntimeConfig、auditMode、timeout | 迁运行时配置 |
|
||||
| **index** | 无 auditMode | auditMode 控制超级个体按钮 | 迁 auditMode |
|
||||
| **gift-pay/detail** | hero-card、requester-card、完整支付流程 | 简单 card | **勿回退**,稳定版更完善 |
|
||||
| **read** | 无 auditMode、无 showShareTip | auditMode、personsConfig、showShareTip | 迁 auditMode |
|
||||
| **my** | profile-name-actions(编辑)、referralEnabled | auditMode 控制支付 UI | **保留**稳定版编辑入口 |
|
||||
| **wallet** | 无 auditMode | auditMode | 迁 auditMode |
|
||||
| **chapters** | 每日新增已删除 | 每日新增区块 | 按需求 |
|
||||
| **about** | ✅ | ✅ | 一致 |
|
||||
| **utils** | ruleEngine | 无 | **保留**稳定版 |
|
||||
|
||||
---
|
||||
|
||||
## 二、需迁移的细节(体验版有、稳定版缺)
|
||||
|
||||
### 2.1 app.js - 运行时配置与审核模式
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **baseUrl 配置** | 硬编码 localhost | getRuntimeBootstrapConfig,extConfig/Storage/默认生产 | **P0** |
|
||||
| **loadRuntimeConfig** | 仅 appId/mchId 等 | 含 apiDomain、auditMode、supportWechat | **P0** |
|
||||
| **globalData** | 无 auditMode | auditMode、supportWechat | **P0** |
|
||||
| **request timeout** | 无 | 15000ms | **P2** |
|
||||
| **loadBookData** | 已类似 | 同 | 已对齐 |
|
||||
|
||||
**新版关键代码(getRuntimeBootstrapConfig):**
|
||||
|
||||
```javascript
|
||||
function getRuntimeBootstrapConfig() {
|
||||
try {
|
||||
const extCfg = wx.getExtConfigSync ? (wx.getExtConfigSync() || {}) : {}
|
||||
return {
|
||||
baseUrl: extCfg.apiBaseUrl || wx.getStorageSync('apiBaseUrl') || DEFAULT_BASE_URL,
|
||||
appId: extCfg.appId || DEFAULT_APP_ID,
|
||||
mchId: extCfg.mchId || DEFAULT_MCH_ID,
|
||||
withdrawSubscribeTmplId: extCfg.withdrawSubscribeTmplId || DEFAULT_WITHDRAW_TMPL_ID
|
||||
}
|
||||
} catch (_) {
|
||||
return { baseUrl: DEFAULT_BASE_URL, appId: DEFAULT_APP_ID, mchId: DEFAULT_MCH_ID, withdrawSubscribeTmplId: DEFAULT_WITHDRAW_TMPL_ID }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**soul-api 支持**:`GET /api/miniprogram/config` 已存在,返回 mpConfig(含 apiDomain、appId、mchId 等)。需在 mp_config 或 defaultMp 中增加 `auditMode`、`supportWechat` 字段,供管理端配置。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 index 页面 - 审核模式
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **超级个体空态按钮** | 始终显示 | 按 auditMode 隐藏 | **P1** |
|
||||
| **data** | 无 auditMode | auditMode: false | - |
|
||||
| **onShow** | 无 | 同步 app.globalData.auditMode | - |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 my 页面 - 审核模式
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **become-member-btn** | 无 auditMode | wx:if="{{!auditMode}}" | **P1** |
|
||||
| **vip-tags** | 无 | wx:if="{{!auditMode}}" | **P1** |
|
||||
| **profile-stat 推荐/收益** | referralEnabled | auditMode | **保留**稳定版 referralEnabled |
|
||||
| **profile-stat 余额** | 无 | wx:if="{{!auditMode}}" | **P1** |
|
||||
| **receive-card** | 无 auditMode | wx:if="{{... && !auditMode}}" | **P1** |
|
||||
| **profile-name-actions** | ✅ 有编辑按钮 | ❌ 无 | **勿回退** |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 wallet 页面 - 审核模式
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **data** | 无 auditMode | auditMode: false | **P1** |
|
||||
| **onLoad** | 无 | 同步 app.globalData.auditMode | **P1** |
|
||||
|
||||
---
|
||||
|
||||
### 2.5 read 页面 - 分享功能与图标
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **操作区分享** | 朋友圈→海报→代付 | 分享→代付→海报 | 按产品需求 |
|
||||
| **share-tip-inline** | 无 | 有「分享后好友购买,你可获得 90% 收益」 | 可选 |
|
||||
| **右下角悬浮按钮** | share.svg + open-type="share" | 🌐 emoji + shareToMoments | 已迁为体验版 |
|
||||
|
||||
---
|
||||
|
||||
### 2.6 read 页面 - 审核模式与配置
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **config 预加载** | linkTags、linkedMiniprograms | 含 personsConfig | **P3** |
|
||||
| **data** | walletBalance、totalSections | auditMode、showShareTip | **P2** auditMode |
|
||||
| **wx.showShareMenu** | menus: ['shareAppMessage','shareTimeline'] | withShareTimeline: true | 均可 |
|
||||
|
||||
---
|
||||
|
||||
## 三、稳定版有、体验版无(勿回退)
|
||||
|
||||
### 3.1 gift-pay/detail
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 说明 |
|
||||
|--------|--------|--------|------|
|
||||
| **UI 结构** | hero-card、requester-card、footer-bar、bg-effects | 简单 card | **勿回退** |
|
||||
| **发起人信息** | 可点击头像跳 member-detail | 无 | **勿回退** |
|
||||
| **跳转文章** | goToArticle 跳 read | 无 | **勿回退** |
|
||||
| **doPay 登录** | 静默 getOpenId,失败再弹 modal | 直接 toast | **勿回退** |
|
||||
| **支付失败** | 识别 prepay 错误并重试 | 简单 toast | **勿回退** |
|
||||
|
||||
### 3.2 my 页面 - 编辑入口
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 说明 |
|
||||
|--------|--------|--------|------|
|
||||
| **profile-name-actions** | ✅ 有编辑按钮 | ❌ 无 | **保留**稳定版 |
|
||||
|
||||
### 3.3 utils - ruleEngine
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 说明 |
|
||||
|--------|--------|--------|------|
|
||||
| **ruleEngine** | ✅ app.js 引用 | ❌ 无 | **保留**稳定版 |
|
||||
|
||||
---
|
||||
|
||||
## 四、迁移优先级建议
|
||||
|
||||
| 优先级 | 迁移项 | 稳定版→体验版 |
|
||||
|--------|--------|---------------|
|
||||
| **P0** | app.js 运行时配置 | 稳定版缺,体验版有 |
|
||||
| **P0** | app.js loadMpConfig 扩展 | 稳定版缺 auditMode,体验版有 |
|
||||
| **P0** | soul-api mp_config | 增加 auditMode、supportWechat |
|
||||
| **P1** | index/my/wallet auditMode | 稳定版缺,体验版有 |
|
||||
| **P2** | app.js request timeout | 稳定版缺,体验版有 |
|
||||
| **P2** | read auditMode | 稳定版缺,体验版有 |
|
||||
| **P3** | read personsConfig、showShareTip | 可选 |
|
||||
|
||||
---
|
||||
|
||||
## 五、不迁移项(维持现状)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| gift-pay/detail 从新版迁 | 稳定版 UI 与逻辑更完善,无需回退 |
|
||||
| 稳定版 ruleEngine | 已迁移,保留 |
|
||||
| 稳定版 profile-name-actions | 保留,新版无 |
|
||||
|
||||
---
|
||||
|
||||
## 六、功能闭环 Checklist(每功能必过)
|
||||
|
||||
```
|
||||
□ 界面:页面、交互、数据绑定
|
||||
□ 接口:API 存在、参数正确、响应格式规范
|
||||
□ 数据:DB/事务/幂等(若涉及)
|
||||
□ 边界:未登录、余额不足、网络失败
|
||||
□ 三端:小程序+后端+管理端(如需)是否都改到
|
||||
□ 保护区域:未动 @/#、分销、支付 核心逻辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、文档产出
|
||||
|
||||
| 文档 | 路径 |
|
||||
|------|------|
|
||||
| **需求采纳清单** | `开发文档/新版迁移-需求采纳清单.md`(勾选是否采纳) |
|
||||
| 功能差异清单 | `开发文档/新版迁移-功能差异清单.md`(本文档) |
|
||||
| 迁移方案/清单 | `开发文档/新版迁移-开发方案与清单.md` |
|
||||
|
||||
---
|
||||
|
||||
**迁移前必做需求评审**:列出功能点 + 样式变更,逐一确认后再迁。先做功能对齐与取舍,评审通过后按最小功能迁移;界面修改先迁、大逻辑排后。
|
||||
159
开发文档/新版迁移-管理端功能差异清单.md
Normal file
159
开发文档/新版迁移-管理端功能差异清单.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# 新版迁移 - 管理端功能差异清单(稳定版 vs 体验版)
|
||||
|
||||
> 乘风分析:稳定版(soul-admin)与体验版(new-soul/soul-admin)的差异,稳定版未迁移完整。
|
||||
> 更新日期:2026-03-17
|
||||
|
||||
**目录说明**:`soul-admin` = 稳定版(生产);`new-soul/soul-admin` = 体验版(新版参考)
|
||||
|
||||
---
|
||||
|
||||
## 一、差异摘要(按页面/模块)
|
||||
|
||||
| 页面/模块 | 稳定版 soul-admin | 体验版 new-soul/soul-admin | 迁移建议 |
|
||||
|-----------|-------------------|---------------------------|----------|
|
||||
| **Settings** | 无审核模式、无 aboutEnabled | 有审核模式、有 aboutEnabled、OSS 完整 | 迁审核模式、aboutEnabled |
|
||||
| **Dashboard** | 无埋点统计、无代付金额、订单固定 5 条 | 有 trackStats、giftedTotal、订单可展开 4→10 | 迁代付金额、订单展开 |
|
||||
| **Content** | chapters/ranking/search/link-person/link-tag/linkedmp | 同 | 一致 |
|
||||
| **路由/布局** | 5 主菜单 + 系统设置 | 同 | 一致 |
|
||||
|
||||
---
|
||||
|
||||
## 二、Settings 页面对比
|
||||
|
||||
### 2.1 小程序审核模式
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **auditMode 开关** | ❌ 无 | ✅ 独立 Card + Switch,单独保存 | **P0** 新增 |
|
||||
| **UI 位置** | - | 小程序配置、OSS 之后,功能开关之前 | - |
|
||||
| **API 传参** | - | `mp_config`(应为 `mpConfig`) | 体验版有 bug,迁时用 mpConfig |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 功能开关
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **找伙伴** | ✅ | ✅ | 一致 |
|
||||
| **推广功能** | ✅ | ✅ | 一致 |
|
||||
| **搜索功能** | ✅ | ✅ | 一致 |
|
||||
| **关于页面** | ❌ 无 | ✅ aboutEnabled | **P1** 新增 |
|
||||
| **保存方式** | 与站点设置一起保存 | 独立 saveFeatureConfigOnly | 体验版更细粒度 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 OSS 配置
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **endpoint** | ✅ | ✅ | 一致 |
|
||||
| **region** | ✅ | ✅ | 一致 |
|
||||
| **accessKeyId** | ✅ | ✅ | 一致 |
|
||||
| **accessKeySecret** | ✅ | ✅ | 一致 |
|
||||
| **bucket** | ✅ | ✅ | 一致 |
|
||||
| **配置状态提示** | ✅ | ✅ | 一致 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 其他 Settings 项
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **链接卡若存客宝密钥** | ✅ ckbLeadApiKey | ✅ | 一致 |
|
||||
| **关于作者** | ✅ | ✅ | 一致 |
|
||||
| **价格设置** | ✅ | ✅ | 一致 |
|
||||
| **小程序配置** | ✅ appId/mchId 等 | ✅ | 一致 |
|
||||
|
||||
---
|
||||
|
||||
## 三、Dashboard 页面对比
|
||||
|
||||
### 3.1 统计卡片
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **总用户数** | ✅ | ✅ | 一致 |
|
||||
| **总收入** | ✅ | ✅ 含 sub「含代付 ¥xx」 | **P1** 迁 giftedTotal |
|
||||
| **订单数** | ✅ | ✅ | 一致 |
|
||||
| **转化率** | ✅ 链接 distribution | ✅ 链接 users | 文案/链接略有不同 |
|
||||
|
||||
---
|
||||
|
||||
### 3.2 最近订单
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **显示条数** | 固定 5 条 | 默认 4 条,可展开至 10 条 | **P1** 迁 ordersExpanded |
|
||||
| **订单类型展示** | 无 balance_recharge | ✅ 支持 balance_recharge、gift_pay | **P2** 迁 formatOrderProduct |
|
||||
| **代付金额汇总** | ❌ 无 | ✅ giftedTotal | **P1** 迁 |
|
||||
|
||||
---
|
||||
|
||||
### 3.3 埋点统计
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 迁移建议 |
|
||||
|--------|--------|--------|----------|
|
||||
| **track/stats** | ❌ 无 | ✅ 调用 /api/admin/track/stats | **P2** 依赖后端接口 |
|
||||
| **period 切换** | - | week/day | - |
|
||||
| **按模块展示** | - | BarChart3 卡片 | - |
|
||||
|
||||
---
|
||||
|
||||
## 四、稳定版独有(勿回退)
|
||||
|
||||
| 功能项 | 稳定版 | 体验版 | 说明 |
|
||||
|--------|--------|--------|------|
|
||||
| **搜索功能文案** | 「首页、目录页搜索栏」 | 「首页搜索栏」 | 稳定版更完整 |
|
||||
| **ChaptersPage** | 有独立文件 | 无(或未用) | App 未挂路由,可能遗留 |
|
||||
|
||||
---
|
||||
|
||||
## 五、迁移优先级建议
|
||||
|
||||
| 优先级 | 迁移项 | 稳定版→体验版 |
|
||||
|--------|--------|---------------|
|
||||
| **P0** | Settings 小程序审核模式 | 稳定版缺,体验版有,需迁 |
|
||||
| **P1** | Settings aboutEnabled | 稳定版缺,体验版有,需迁 |
|
||||
| **P1** | Dashboard giftedTotal + ordersExpanded | 稳定版缺,体验版有,需迁 |
|
||||
| **P2** | Dashboard 埋点统计 | 依赖 track/stats 接口 |
|
||||
| **P2** | formatOrderProduct balance_recharge | 稳定版缺,体验版有,需迁 |
|
||||
|
||||
---
|
||||
|
||||
## 六、接口与数据层检查
|
||||
|
||||
| 接口 | 稳定版 | 体验版 | soul-api 支持 |
|
||||
|------|--------|--------|---------------|
|
||||
| GET /api/admin/settings | ✅ | ✅ | ✅ |
|
||||
| POST /api/admin/settings | ✅ 传 mpConfig | ✅ 传 mp_config(bug) | ✅,需 camelCase |
|
||||
| GET /api/admin/track/stats | ❌ 未用 | ✅ period=week/day | 需确认 |
|
||||
| GET /api/admin/dashboard/stats | ✅ | ✅ | ✅ |
|
||||
| GET /api/admin/dashboard/overview | ✅ | ✅ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 七、功能闭环 Checklist(每功能必过)
|
||||
|
||||
```
|
||||
□ 界面:页面、交互、数据绑定
|
||||
□ 接口:API 存在、参数正确、响应格式规范
|
||||
□ 数据:DB/配置存储(system_config 表)
|
||||
□ 边界:保存失败、加载失败
|
||||
□ 三端:管理端 + soul-api(mp_config 需供小程序 config 读取)
|
||||
□ 保护区域:未动分销、支付核心逻辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、文档产出
|
||||
|
||||
| 文档 | 路径 |
|
||||
|------|------|
|
||||
| **需求采纳清单** | `开发文档/新版迁移-需求采纳清单.md`(勾选是否采纳) |
|
||||
| 管理端功能差异清单 | `开发文档/新版迁移-管理端功能差异清单.md`(本文档) |
|
||||
| 小程序功能差异清单 | `开发文档/新版迁移-功能差异清单.md` |
|
||||
| 迁移完成度 | `开发文档/迁移完成度与待办清单.md` |
|
||||
|
||||
---
|
||||
|
||||
**迁移前必做需求评审**:列出功能点 + 样式变更,逐一确认后再迁。
|
||||
61
开发文档/新版迁移-需求采纳清单.md
Normal file
61
开发文档/新版迁移-需求采纳清单.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 新版迁移 - 需求采纳清单
|
||||
|
||||
> 推荐迁移项汇总,供选择是否采纳。勾选 ☐ 表示采纳,☑ 表示已确认采纳。
|
||||
> 更新日期:2026-03-17
|
||||
|
||||
**标签说明**:
|
||||
- **🔴 必选**:提审/生产必备,建议优先
|
||||
- **🟠 推荐**:有明显价值,建议采纳
|
||||
- **🟡 可选**:按需采纳
|
||||
- **⚪ 不迁**:明确不迁移
|
||||
|
||||
**乘风建议(2026-03-17)**:X1/X2/X7 与审核模式配套必迁;X8/X9 低成本建议采纳;M6 因 soul-api 暂无 track/stats 接口,待接口就绪后再迁;X10 保持不迁。
|
||||
|
||||
---
|
||||
|
||||
## 一、管理端(soul-admin)
|
||||
|
||||
| 序号 | 功能项 | 标签 | 简要说明 | 采纳 |
|
||||
|:----:|--------|------|----------|:----:|
|
||||
| M1 | 小程序审核模式 | 🔴 必选 | 系统设置 → 审核模式开关,提审前隐藏支付入口 | ☑ |
|
||||
| M2 | 关于页面开关 aboutEnabled | 🟠 推荐 | 功能开关第 4 项,控制关于页访问 | ☑ |
|
||||
| M3 | Dashboard 代付金额 giftedTotal | 🟠 推荐 | 总收入下显示「含代付 ¥xx」 | ☑ |
|
||||
| M4 | Dashboard 订单展开 | 🟠 推荐 | 默认 4 条,可展开至 10 条 | ☑ |
|
||||
| M5 | 订单类型 balance_recharge | 🟡 可选 | 订单列表正确展示余额充值类型 | ☑ |
|
||||
| M6 | 埋点统计 track/stats | 🟡 可选 | 依赖 /api/admin/track/stats(soul-api 暂无,待接口就绪) | ☐ |
|
||||
|
||||
---
|
||||
|
||||
## 二、小程序(miniprogram)
|
||||
|
||||
| 序号 | 功能项 | 标签 | 简要说明 | 采纳 |
|
||||
|:----:|--------|------|----------|:----:|
|
||||
| X1 | app.js 运行时配置 | 🔴 必选 | getRuntimeBootstrapConfig,避免生产用 localhost | ☑ |
|
||||
| X2 | app.js loadMpConfig 扩展 | 🔴 必选 | 支持 apiDomain、auditMode、supportWechat | ☑ |
|
||||
| X3 | soul-api mp_config | 🔴 必选 | 增加 auditMode、supportWechat 字段 | ☑ |
|
||||
| X4 | index 超级个体按 auditMode 隐藏 | 🟠 推荐 | 审核时隐藏超级个体空态按钮 | ☑ |
|
||||
| X5 | my 页支付相关按 auditMode 隐藏 | 🟠 推荐 | 会员、收益、余额、一键收款 | ☑ |
|
||||
| X6 | wallet 页 auditMode | 🟠 推荐 | 审核时隐藏或提示 | ☑ |
|
||||
| X7 | read 页 auditMode | 🟠 推荐 | 购买按钮等按审核隐藏 | ☑ |
|
||||
| X8 | app.js request timeout | 🟡 可选 | 15s 超时,避免长时间挂起 | ☑ |
|
||||
| X9 | read share-tip-inline | 🟡 可选 | 「分享后好友购买,你可获得 90% 收益」 | ☑ |
|
||||
| X10 | read personsConfig、showShareTip | ⚪ 不迁 | 按需,优先级低 | ☐ |
|
||||
|
||||
---
|
||||
|
||||
## 三、不迁移项(维持现状)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| gift-pay/detail 从体验版迁 | 稳定版 UI 与逻辑更完善 |
|
||||
| 稳定版 ruleEngine | 保留 |
|
||||
| 稳定版 profile-name-actions | 保留编辑入口 |
|
||||
| 稳定版 referralEnabled | 保留,与 auditMode 并存 |
|
||||
|
||||
---
|
||||
|
||||
## 四、采纳后执行
|
||||
|
||||
1. 将 ☐ 改为 ☑ 表示确认采纳
|
||||
2. 按标签优先级排期:🔴 → 🟠 → 🟡
|
||||
3. 迁移完成后执行 `change-checklist` 三端关联检查
|
||||
@@ -83,6 +83,17 @@
|
||||
|
||||
原搁置项富文本/打包引导/存客宝均已确认稳定版已有,无新增搁置。
|
||||
|
||||
### 新版细节未迁移(2026-03-17 乘风分析)
|
||||
|
||||
| 项 | 说明 | 优先级 |
|
||||
|----|------|:------:|
|
||||
| 运行时配置 | getRuntimeBootstrapConfig、extConfig/Storage、默认生产 baseUrl | P0 |
|
||||
| loadMpConfig 扩展 | apiDomain、auditMode、supportWechat | P0 |
|
||||
| auditMode 审核模式 | index/my/wallet/read 支付相关 UI 按审核隐藏 | P1 |
|
||||
| request timeout | app.request 增加 timeout | P2 |
|
||||
|
||||
详见 `开发文档/新版迁移-功能差异清单.md`(小程序)、`开发文档/新版迁移-管理端功能差异清单.md`(管理端)。
|
||||
|
||||
### 规则引擎(2026-03-17 已迁移)
|
||||
|
||||
| 项 | 状态 |
|
||||
@@ -93,11 +104,11 @@
|
||||
| after_match(匹配后引导) | ✅ match.js reportMatch 后触发 |
|
||||
| user_rules 表 + 默认规则 | ✅ AutoMigrate + add-user-rules-default.sql |
|
||||
|
||||
### 埋点(待补充)
|
||||
### 埋点(已补齐 2026-03-17)
|
||||
|
||||
| 项 | 当前状态 | 待办 |
|
||||
|----|----------|------|
|
||||
| **埋点 trackClick** | 已接入:chapters、read、wallet | 遗漏:index、my、match、vip、search、referral 等页 |
|
||||
| 项 | 当前状态 |
|
||||
|----|----------|
|
||||
| **埋点 trackClick** | 已接入:index、my、match、vip、search、referral、chapters、read、wallet |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user