feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
/* AI_CHANGELOG
|
||||
| 日期 | Prompt | 变更 |
|
||||
|------|--------|------|
|
||||
| 2026-03-23 | 角色路由+页面权限守卫 | onShow 添加 checkPageAccess 权限守卫 |
|
||||
*/
|
||||
import { checkPageAccess } from '../../utils/auth-guard'
|
||||
import { fetchCustomerDetail } from '../../services/api'
|
||||
|
||||
interface ConsumptionRecord {
|
||||
@@ -28,218 +34,50 @@ interface ConsumptionRecord {
|
||||
}
|
||||
|
||||
const mockRecords: ConsumptionRecord[] = [
|
||||
{
|
||||
id: "r1",
|
||||
type: "table",
|
||||
date: "2026-02-05",
|
||||
tableName: "A12号台",
|
||||
startTime: "21:30",
|
||||
endTime: "00:50",
|
||||
duration: "3h 20min",
|
||||
tableFee: 180,
|
||||
tableOrigPrice: 240,
|
||||
coaches: [
|
||||
{ name: "小燕", level: "senior", levelColor: "pink", courseType: "基础课", hours: "2.5h", fee: 200 },
|
||||
{ name: "Amy", level: "junior", levelColor: "green", courseType: "激励课", hours: "0.5h", perfHours: "1h", fee: 50 },
|
||||
],
|
||||
foodAmount: 210,
|
||||
foodOrigPrice: 260,
|
||||
totalAmount: 640,
|
||||
totalOrigPrice: 750,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
type: "table",
|
||||
date: "2026-02-01",
|
||||
tableName: "888号台",
|
||||
startTime: "14:00",
|
||||
endTime: "16:00",
|
||||
duration: "2h 00min",
|
||||
tableFee: 120,
|
||||
coaches: [
|
||||
{ name: "泡芙", level: "middle", levelColor: "purple", courseType: "激励课", hours: "1.5h", perfHours: "2h", fee: 100 },
|
||||
],
|
||||
totalAmount: 220,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
type: "shop",
|
||||
date: "2026-01-28",
|
||||
coaches: [
|
||||
{ name: "小燕", level: "senior", levelColor: "pink", courseType: "基础课", hours: "1h", fee: 100 },
|
||||
],
|
||||
foodAmount: 180,
|
||||
totalAmount: 280,
|
||||
},
|
||||
{ id: '', type: 'table', date: '', tableName: '', startTime: '', endTime: '', duration: '', tableFee: 0, coaches: [{ name: '', level: '', levelColor: '', courseType: '', hours: '', fee: 0 }], foodAmount: 0, totalAmount: 0, payMethod: '' },
|
||||
{ id: '', type: 'recharge', date: '', rechargeAmount: 0 },
|
||||
]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: "loading" as "loading" | "empty" | "error" | "normal",
|
||||
detail: {
|
||||
id: "cust_001",
|
||||
name: "王先生",
|
||||
avatarChar: "王",
|
||||
phone: "13812345678",
|
||||
balance: "8,600",
|
||||
consumption60d: "2,800",
|
||||
idealInterval: "7天",
|
||||
daysSinceVisit: "12天",
|
||||
id: '',
|
||||
name: '',
|
||||
avatarChar: '',
|
||||
phone: '',
|
||||
balance: null as number | null,
|
||||
consumption60d: null as number | null,
|
||||
idealInterval: null as number | null,
|
||||
daysSinceVisit: null as number | null,
|
||||
},
|
||||
phoneVisible: false,
|
||||
aiColor: "indigo" as "red" | "orange" | "yellow" | "blue" | "indigo" | "purple",
|
||||
aiInsight: {
|
||||
summary: "高价值 VIP 客户,月均到店 4-5 次,偏好夜场中式台球,近期对斯诺克产生兴趣。社交属性强,常带固定球搭子,有拉新能力。储值余额充足,对促销活动响应积极。",
|
||||
summary: '',
|
||||
strategies: [
|
||||
{ color: "green", text: "最后到店距今 12 天,超出理想间隔 7 天,建议尽快安排助教小燕主动联系召回" },
|
||||
{ color: "amber", text: "客户提到想练斯诺克走位,可推荐斯诺克专项课程包,结合储值优惠提升客单价" },
|
||||
{ color: "pink", text: "社交属性强,可邀请参加门店球友赛事活动,带动球搭子到店消费" },
|
||||
{ color: '', text: '' },
|
||||
{ color: '', text: '' },
|
||||
],
|
||||
},
|
||||
clues: [
|
||||
{
|
||||
category: "客户\n基础",
|
||||
categoryColor: "primary",
|
||||
text: "🎂 生日 3月15日 · VIP会员 · 注册2年",
|
||||
source: "系统",
|
||||
},
|
||||
{
|
||||
category: "消费\n习惯",
|
||||
categoryColor: "success",
|
||||
text: "🌙 常来夜场 · 月均4-5次",
|
||||
source: "系统",
|
||||
},
|
||||
{
|
||||
category: "消费\n习惯",
|
||||
categoryColor: "success",
|
||||
text: "💰 高客单价",
|
||||
source: "系统",
|
||||
detail: "近60天场均消费 ¥420,高于门店均值 ¥180;偏好夜场时段,酒水附加消费占比 35%",
|
||||
},
|
||||
{
|
||||
category: "玩法\n偏好",
|
||||
categoryColor: "purple",
|
||||
text: "🎱 偏爱中式 · 斯诺克进阶中",
|
||||
source: "系统",
|
||||
},
|
||||
{
|
||||
category: "促销\n接受",
|
||||
categoryColor: "warning",
|
||||
text: "🍷 爱点酒水套餐 · 对储值活动敏感",
|
||||
source: "系统",
|
||||
detail: "最近3次到店均点了酒水套餐;上次 ¥5000 储值活动当天即充值,对满赠类活动响应率高",
|
||||
},
|
||||
{
|
||||
category: "社交\n关系",
|
||||
categoryColor: "pink",
|
||||
text: "👥 常带朋友 · 固定球搭子2人",
|
||||
source: "系统",
|
||||
detail: "近60天 80% 的到店为多人局,常与「李哥」「阿杰」同行;曾介绍2位新客办卡",
|
||||
},
|
||||
{
|
||||
category: "重要\n反馈",
|
||||
categoryColor: "error",
|
||||
text: "⚠️ 上次提到想练斯诺克走位,对球桌维护质量比较在意,建议优先安排VIP房",
|
||||
source: "小燕",
|
||||
},
|
||||
{ category: '', categoryColor: '', text: '', source: '' },
|
||||
{ category: '', categoryColor: '', text: '', source: '', detail: '' },
|
||||
],
|
||||
coachTasks: [
|
||||
{
|
||||
name: "小燕",
|
||||
level: "senior",
|
||||
levelColor: "pink",
|
||||
taskType: "高优先召回",
|
||||
taskColor: "red",
|
||||
bgClass: "coach-card-red",
|
||||
status: "normal",
|
||||
lastService: "02-20 21:30 · 2.5h",
|
||||
metrics: [
|
||||
{ label: "近60天次数", value: "18次", color: "primary" },
|
||||
{ label: "总时长", value: "17h" },
|
||||
{ label: "次均时长", value: "0.9h", color: "warning" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "泡芙",
|
||||
level: "middle",
|
||||
levelColor: "purple",
|
||||
taskType: "优先召回",
|
||||
taskColor: "orange",
|
||||
bgClass: "coach-card-orange",
|
||||
status: "pinned",
|
||||
lastService: "02-15 14:00 · 1.5h",
|
||||
metrics: [
|
||||
{ label: "近60天次数", value: "12次", color: "primary" },
|
||||
{ label: "总时长", value: "11h" },
|
||||
{ label: "次均时长", value: "0.9h", color: "warning" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Amy",
|
||||
level: "junior",
|
||||
levelColor: "green",
|
||||
taskType: "关系构建",
|
||||
taskColor: "pink",
|
||||
bgClass: "coach-card-pink",
|
||||
status: "normal",
|
||||
lastService: "02-10 19:00 · 1.0h",
|
||||
metrics: [
|
||||
{ label: "近60天次数", value: "8次", color: "primary" },
|
||||
{ label: "总时长", value: "6h" },
|
||||
{ label: "次均时长", value: "0.75h", color: "warning" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Lucy",
|
||||
level: "senior",
|
||||
levelColor: "pink",
|
||||
taskType: "客户回访",
|
||||
taskColor: "teal",
|
||||
bgClass: "coach-card-teal",
|
||||
status: "abandoned",
|
||||
lastService: "01-28 20:30 · 2.0h",
|
||||
metrics: [
|
||||
{ label: "近60天次数", value: "6次", color: "primary" },
|
||||
{ label: "总时长", value: "9h" },
|
||||
{ label: "次均时长", value: "1.5h", color: "warning" },
|
||||
],
|
||||
},
|
||||
{ name: '', level: '', levelColor: '', taskType: '', taskColor: '', bgClass: '', status: '', lastService: '', metrics: [{ label: '', value: '' }] },
|
||||
{ name: '', level: '', levelColor: '', taskType: '', taskColor: '', bgClass: '', status: '', lastService: '', metrics: [{ label: '', value: '' }] },
|
||||
],
|
||||
favoriteCoaches: [
|
||||
{
|
||||
emoji: "❤️",
|
||||
name: "小燕",
|
||||
relationIndex: "9.2",
|
||||
indexColor: "success",
|
||||
bgClass: "fav-card-pink",
|
||||
stats: [
|
||||
{ label: "基础", value: "12h", color: "primary" },
|
||||
{ label: "激励", value: "5h", color: "warning" },
|
||||
{ label: "上课", value: "18次" },
|
||||
{ label: "充值", value: "¥5,000", color: "success" },
|
||||
],
|
||||
},
|
||||
{
|
||||
emoji: "💛",
|
||||
name: "泡芙",
|
||||
relationIndex: "7.8",
|
||||
indexColor: "warning",
|
||||
bgClass: "fav-card-amber",
|
||||
stats: [
|
||||
{ label: "基础", value: "8h", color: "primary" },
|
||||
{ label: "激励", value: "3h", color: "warning" },
|
||||
{ label: "上课", value: "12次" },
|
||||
{ label: "充值", value: "¥3,000", color: "success" },
|
||||
],
|
||||
},
|
||||
{ emoji: '', name: '', relationIndex: '', indexColor: '', bgClass: '', stats: [{ label: '', value: '' }] },
|
||||
],
|
||||
consumptionRecords: mockRecords,
|
||||
loadingMore: false,
|
||||
noteModalVisible: false,
|
||||
favCoachExpanded: false,
|
||||
sortedNotes: [
|
||||
{ id: 'n1', tagLabel: '管理员', createdAt: '2026-03-05 14:30', content: '本月到店积极,对斯诺克课程感兴趣,建议持续跟进推荐相关课程包' },
|
||||
{ id: 'n2', tagLabel: '小燕', createdAt: '2026-02-20 16:45', content: '客户反馈服务态度很好,提到下次想带朋友一起来' },
|
||||
{ id: 'n3', tagLabel: '管理员', createdAt: '2026-02-10 10:00', content: '上次储值活动当天即充值 ¥5000,对满赠类活动响应积极' },
|
||||
{ id: '', tagLabel: '', createdAt: '', content: '' },
|
||||
{ id: '', tagLabel: '', createdAt: '', content: '' },
|
||||
] as Array<{ id: string; tagLabel: string; createdAt: string; content: string }>,
|
||||
},
|
||||
|
||||
@@ -252,25 +90,50 @@ Page({
|
||||
this.loadDetail(id)
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 权限守卫:检查登录状态、账号禁用、角色权限
|
||||
checkPageAccess('pages/customer-detail/customer-detail')
|
||||
},
|
||||
|
||||
// CHANGE 2026-03-29 | P3 联调:映射所有后端返回字段(AI 相关暂跳过)
|
||||
async loadDetail(id?: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
wx.showLoading({ title: '加载中...', mask: true })
|
||||
try {
|
||||
if (id) {
|
||||
const detail = await fetchCustomerDetail(id)
|
||||
if (detail) {
|
||||
const d = await fetchCustomerDetail(id)
|
||||
if (d) {
|
||||
this.setData({
|
||||
detail: {
|
||||
...this.data.detail,
|
||||
id: detail.id ?? id,
|
||||
name: detail.name || this.data.detail.name,
|
||||
phone: detail.phone || this.data.detail.phone,
|
||||
id: d.id ?? id,
|
||||
name: d.name || '',
|
||||
avatarChar: (d.name || '')[0] || '',
|
||||
phone: d.phone || '',
|
||||
balance: d.balance ?? null,
|
||||
// CHANGE 2026-03-29 | Pydantic 把 consumption_60d → consumption60D(大写 D)
|
||||
consumption60d: d.consumption60D ?? d.consumption60d ?? null,
|
||||
idealInterval: d.idealInterval ?? null,
|
||||
daysSinceVisit: d.daysSinceVisit ?? null,
|
||||
},
|
||||
// 维客线索
|
||||
clues: d.retentionClues || [],
|
||||
// 助教任务
|
||||
coachTasks: d.coachTasks || [],
|
||||
// 最亲密助教
|
||||
favoriteCoaches: d.favoriteCoaches || [],
|
||||
// 消费记录
|
||||
consumptionRecords: d.consumptionRecords || [],
|
||||
// 备注
|
||||
sortedNotes: d.notes || [],
|
||||
})
|
||||
}
|
||||
}
|
||||
this.setData({ pageState: 'normal' })
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('[customer-detail] loadDetail 失败:', e)
|
||||
this.setData({ pageState: 'error' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -298,7 +161,7 @@ Page({
|
||||
onViewServiceRecords() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/customer-service-records/customer-service-records?customerId=${customerId}`,
|
||||
url: `/pages/customer-records/customer-records?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
@@ -315,11 +178,64 @@ Page({
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
onNoteConfirm(e: any) {
|
||||
// CHANGE 2026-03-29 | 备注创建调用后端 API,保存后直接插入列表顶部
|
||||
async onNoteConfirm(e: any) {
|
||||
const { content, score } = e.detail || {}
|
||||
this.setData({ noteModalVisible: false })
|
||||
if (!content) return
|
||||
try {
|
||||
const { createNote } = require('../../services/api')
|
||||
const result = await createNote({
|
||||
targetId: Number(this.data.detail.id),
|
||||
content,
|
||||
score: score || undefined,
|
||||
})
|
||||
wx.showToast({ title: '备注已保存', icon: 'success' })
|
||||
// 直接插入到列表顶部,不刷新整页
|
||||
const now = new Date()
|
||||
const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
const newNote = {
|
||||
id: result?.id || Date.now(),
|
||||
tagLabel: '备注',
|
||||
createdAt: timeStr,
|
||||
content,
|
||||
}
|
||||
this.setData({
|
||||
sortedNotes: [newNote, ...this.data.sortedNotes],
|
||||
})
|
||||
} catch {
|
||||
wx.showToast({ title: '备注保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
onNoteCancel() {
|
||||
this.setData({ noteModalVisible: false })
|
||||
},
|
||||
|
||||
onToggleFavCoaches() {
|
||||
this.setData({ favCoachExpanded: !this.data.favCoachExpanded })
|
||||
},
|
||||
|
||||
onDeleteNote(e: WechatMiniprogram.TouchEvent) {
|
||||
const noteId = e.currentTarget.dataset.id
|
||||
if (!noteId) return
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '删除后不可恢复',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
const { deleteNote } = require('../../services/api')
|
||||
await deleteNote(noteId)
|
||||
// 从列表中移除
|
||||
this.setData({
|
||||
sortedNotes: this.data.sortedNotes.filter((n: any) => n.id !== noteId),
|
||||
})
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
} catch {
|
||||
wx.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user