diff --git a/server/cmd/seed/main.go b/server/cmd/seed/main.go new file mode 100644 index 0000000..60d583c --- /dev/null +++ b/server/cmd/seed/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "log" + "time" + + "golang.org/x/crypto/bcrypt" + + "kefu-sys/server/internal/config" + "kefu-sys/server/internal/model" +) + +func main() { + cfg := config.Load() + model.InitDB(cfg.Database.DSN()) + + seed() + log.Println("种子数据填充完成") +} + +func seed() { + hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + pwd := string(hash) + + // Plans + plans := []model.Plan{ + {Name: "基础版", PriceMonthly: 299, Seats: 2, StorageDays: 30, KBLimit: 50, Status: "active", Features: `{"stats":"basic","api":false,"channels":["web"],"brand":false}`}, + {Name: "专业版", PriceMonthly: 699, Seats: 10, StorageDays: 180, KBLimit: 500, Status: "active", Features: `{"stats":"advanced","api":true,"channels":["web","wechat","app"],"brand":true}`}, + {Name: "企业版", PriceMonthly: 1999, Seats: 50, StorageDays: 0, KBLimit: 0, Status: "active", Features: `{"stats":"custom","api":true,"channels":["all"],"brand":true,"dedicated":true}`}, + } + model.DB.Create(&plans) + + // Tenants + now := time.Now() + tenants := []model.Tenant{ + {Name: "赵六科技", PlanID: &plans[2].ID, SeatCount: 30, ExpireAt: now.AddDate(1, 0, 0), Status: "normal", ContactName: "赵总", ContactPhone: "13800001111", ContactEmail: "zhao@tech.com"}, + {Name: "李四商贸", PlanID: &plans[1].ID, SeatCount: 10, ExpireAt: now.AddDate(0, 6, 0), Status: "normal", ContactName: "李经理", ContactPhone: "13900002222", ContactEmail: "li@trade.com"}, + {Name: "王五集团", PlanID: &plans[2].ID, SeatCount: 50, ExpireAt: now.AddDate(0, 0, 7), Status: "expiring", ContactName: "王总", ContactPhone: "13700003333", ContactEmail: "wang@group.com"}, + {Name: "孙八信息", PlanID: &plans[0].ID, SeatCount: 2, ExpireAt: now.AddDate(1, 0, 0), Status: "suspended", ContactName: "孙经理", ContactPhone: "13600004444", ContactEmail: "sun@info.com"}, + {Name: "周九科技", PlanID: &plans[1].ID, SeatCount: 8, ExpireAt: now.AddDate(0, -1, 0), Status: "expired", ContactName: "周总", ContactPhone: "13500005555", ContactEmail: "zhou@tech.com"}, + } + model.DB.Create(&tenants) + + // Platform admin + model.DB.Create(&model.User{ + TenantID: 0, Role: "platform_admin", Username: "platform_admin", PasswordHash: pwd, Nickname: "平台管理员", Status: "online", + }) + + // Tenant 1 users + model.DB.Create(&model.User{ + TenantID: tenants[0].ID, Role: "admin", Username: "admin", PasswordHash: pwd, Nickname: "赵总(管理员)", Status: "online", + }) + model.DB.Create(&model.User{ + TenantID: tenants[0].ID, Role: "supervisor", Username: "supervisor", PasswordHash: pwd, Nickname: "客服主管", Status: "online", + }) + agent1 := model.User{TenantID: tenants[0].ID, Role: "agent", Username: "agent1", PasswordHash: pwd, Nickname: "客服小王", Status: "online"} + agent2 := model.User{TenantID: tenants[0].ID, Role: "agent", Username: "agent2", PasswordHash: pwd, Nickname: "客服小李", Status: "online"} + agent3 := model.User{TenantID: tenants[0].ID, Role: "agent", Username: "agent3", PasswordHash: pwd, Nickname: "客服小张", Status: "online"} + model.DB.Create(&[]model.User{agent1, agent2, agent3}) + + // Tenant 2 users + model.DB.Create(&model.User{ + TenantID: tenants[1].ID, Role: "admin", Username: "litrade_admin", PasswordHash: pwd, Nickname: "李经理", Status: "online", + }) + model.DB.Create(&model.User{ + TenantID: tenants[1].ID, Role: "agent", Username: "litrade_agent1", PasswordHash: pwd, Nickname: "客服小李2", Status: "online", + }) + + // Channels for Tenant 1 + channels := []model.Channel{ + {TenantID: tenants[0].ID, Type: "web", Name: "网页聊天", Status: "enabled", ScriptCode: ``}, + {TenantID: tenants[0].ID, Type: "wechat", Name: "微信公众号", Status: "enabled"}, + {TenantID: tenants[0].ID, Type: "app", Name: "APP内嵌", Status: "disabled"}, + } + model.DB.Create(&channels) + + // Customers + customers := []model.Customer{ + {TenantID: tenants[0].ID, Name: "张三", Phone: "138****8888", Email: "zhang@example.com", Tags: `["VIP客户","新客户"]`, Source: "网页", Status: "online", ConversationCount: 12}, + {TenantID: tenants[0].ID, Name: "李四", Phone: "139****7777", Email: "li@example.com", Tags: `["活跃"]`, Source: "微信", Status: "offline", ConversationCount: 8}, + {TenantID: tenants[0].ID, Name: "王五", Phone: "137****6666", Email: "wang@example.com", Tags: `["企业客户","VIP客户"]`, Source: "APP", Status: "busy", ConversationCount: 25}, + {TenantID: tenants[0].ID, Name: "赵六科技", Phone: "136****5555", Email: "zhao@tech.com", Tags: `["企业客户"]`, Source: "网页", Status: "online", ConversationCount: 6}, + {TenantID: tenants[0].ID, Name: "钱七", Phone: "135****4444", Tags: `["沉默"]`, Source: "邮件", Status: "offline", ConversationCount: 3}, + {TenantID: tenants[0].ID, Name: "孙八", Phone: "134****3333", Email: "sun@example.com", Tags: `["新客户","活跃"]`, Source: "网页", Status: "online", ConversationCount: 2}, + } + model.DB.Create(&customers) + + // Sessions + sessions := []model.Session{ + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[0].ID, AgentID: &agent1.ID, Status: "active", Priority: "urgent"}, + {TenantID: tenants[0].ID, ChannelID: channels[1].ID, CustomerID: customers[1].ID, AgentID: &agent2.ID, Status: "active", Priority: "normal"}, + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[2].ID, AgentID: nil, Status: "waiting", Priority: "normal"}, + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[3].ID, AgentID: &agent3.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(5)}, + {TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[4].ID, AgentID: &agent1.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(4)}, + } + model.DB.Create(&sessions) + + // Messages + messages := []model.Message{ + {SessionID: sessions[0].ID, SenderType: "visitor", SenderID: &customers[0].ID, Content: "你好,我的订单怎么还没发货?", Type: "text", Seq: 1, SentAt: now}, + {SessionID: sessions[0].ID, SenderType: "agent", SenderID: &agent1.ID, Content: "您好,请提供一下您的订单号,我帮您查看", Type: "text", Seq: 2, SentAt: now}, + {SessionID: sessions[0].ID, SenderType: "visitor", SenderID: &customers[0].ID, Content: "订单号 AB20260714001", Type: "text", Seq: 3, SentAt: now}, + } + model.DB.Create(&messages) + + // Knowledge categories & entries + cats := []model.Category{ + {TenantID: tenants[0].ID, Name: "产品常见问题"}, + {TenantID: tenants[0].ID, Name: "售后服务"}, + {TenantID: tenants[0].ID, Name: "技术支持"}, + {TenantID: tenants[0].ID, Name: "快捷回复模板"}, + } + model.DB.Create(&cats) + entries := []model.KnowledgeEntry{ + {TenantID: tenants[0].ID, CategoryID: cats[0].ID, Title: "如何修改登录密码", Content: "登录后在右上角头像→个人设置→修改密码", Status: "published", UsageCount: 156}, + {TenantID: tenants[0].ID, CategoryID: cats[0].ID, Title: "支持哪些支付方式", Content: "支持微信支付、支付宝、银行转账", Status: "published", UsageCount: 98}, + {TenantID: tenants[0].ID, CategoryID: cats[1].ID, Title: "退货流程说明", Content: "在线申请→审核→寄回→退款,全程3-5个工作日", Status: "published", UsageCount: 45}, + {TenantID: tenants[0].ID, CategoryID: cats[2].ID, Title: "API接口文档", Content: "开发者文档请访问 docs.example.com/api", Status: "draft", UsageCount: 0}, + {TenantID: tenants[0].ID, CategoryID: cats[3].ID, Title: "欢迎语模板", Content: "您好!欢迎来到客服云,请问有什么可以帮您的?", Status: "published", UsageCount: 230}, + } + model.DB.Create(&entries) + + // Announcements + model.DB.Create(&[]model.Announcement{ + {Title: "系统维护通知", Content: "平台将于7月20日 02:00-04:00 进行例行维护", Status: "published"}, + {Title: "新功能上线", Content: "知识库批量导入功能已上线", Status: "published"}, + }) +} + +func ptr[T any](v T) *T { return &v } diff --git a/web/src/pages/agent/Customers.tsx b/web/src/pages/agent/Customers.tsx index 8c40fa5..1088755 100644 --- a/web/src/pages/agent/Customers.tsx +++ b/web/src/pages/agent/Customers.tsx @@ -1,30 +1,7 @@ -import { useState } from 'react' -import { Table, Input, Select, Tag, Drawer, Button, Space, Badge, Descriptions, Tabs, Empty } from 'antd' +import { useState, useEffect } from 'react' +import { Table, Input, Select, Tag, Drawer, Button, Space, Badge, Descriptions, Empty, message } from 'antd' import { SearchOutlined, PhoneOutlined, MailOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons' - -interface Customer { - id: string - name: string - phone: string - email: string - tags: string[] - status: 'online' | 'offline' | 'busy' - source: string - conversationCount: number - satisfaction: number - pending: number - lastContact: string -} - -const mockCustomers: Customer[] = [ - { id: '1', name: '张三', phone: '138****8888', email: 'zhang@example.com', tags: ['VIP客户', '新客户'], status: 'online', source: '网页', conversationCount: 12, satisfaction: 4.8, pending: 0, lastContact: '2026-07-14 10:32' }, - { id: '2', name: '李四', phone: '139****7777', email: 'li@example.com', tags: ['活跃'], status: 'offline', source: '微信', conversationCount: 8, satisfaction: 4.5, pending: 2, lastContact: '2026-07-14 09:15' }, - { id: '3', name: '王五', phone: '137****6666', email: 'wang@example.com', tags: ['企业客户', 'VIP客户'], status: 'busy', source: 'APP', conversationCount: 25, satisfaction: 4.2, pending: 1, lastContact: '2026-07-14 11:00' }, - { id: '4', name: '赵六科技', phone: '136****5555', email: 'zhao@tech.com', tags: ['企业客户'], status: 'online', source: '网页', conversationCount: 6, satisfaction: 4.9, pending: 0, lastContact: '2026-07-13 16:45' }, - { id: '5', name: '钱七', phone: '135****4444', email: '', tags: ['沉默'], status: 'offline', source: '邮件', conversationCount: 3, satisfaction: 3.5, pending: 0, lastContact: '2026-07-10 14:20' }, - { id: '6', name: '孙八', phone: '134****3333', email: 'sun@example.com', tags: ['新客户', '活跃'], status: 'online', source: '网页', conversationCount: 2, satisfaction: 0, pending: 3, lastContact: '2026-07-14 10:30' }, - { id: '7', name: '周九', phone: '133****2222', email: 'zhou@example.com', tags: ['VIP客户'], status: 'busy', source: '微信', conversationCount: 18, satisfaction: 4.7, pending: 0, lastContact: '2026-07-14 09:50' }, -] +import { getCustomers, type Customer } from '@/services/api' const statusMap: Record = { online: { color: 'green', text: '在线' }, @@ -33,140 +10,100 @@ const statusMap: Record = { } const tagColors: Record = { - 'VIP客户': 'gold', - '新客户': 'blue', - '活跃': 'green', - '沉默': 'default', - '企业客户': 'purple', + 'VIP客户': 'gold', '新客户': 'blue', '活跃': 'green', '沉默': 'default', '企业客户': 'purple', } const Customers = () => { + const [customers, setCustomers] = useState([]) + const [loading, setLoading] = useState(true) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) const [search, setSearch] = useState('') - const [tagFilter, setTagFilter] = useState([]) const [statusFilter, setStatusFilter] = useState([]) - const [sourceFilter, setSourceFilter] = useState([]) const [selectedCustomer, setSelectedCustomer] = useState(null) const [drawerOpen, setDrawerOpen] = useState(false) - const filtered = mockCustomers.filter(c => { - if (search && !c.name.includes(search) && !c.phone.includes(search) && !c.email.includes(search)) return false - if (tagFilter.length > 0 && !tagFilter.some(t => c.tags.includes(t))) return false - if (statusFilter.length > 0 && !statusFilter.includes(c.status)) return false - if (sourceFilter.length > 0 && !sourceFilter.includes(c.source)) return false - return true - }) + useEffect(() => { + loadCustomers() + }, [page, search, statusFilter]) + + const loadCustomers = async () => { + setLoading(true) + try { + const res = await getCustomers({ search, status: statusFilter[0], page }) + setCustomers(res.list) + setTotal(res.total) + } catch { + setCustomers([]) + } finally { + setLoading(false) + } + } + + const parseTags = (tagsStr: string): string[] => { + try { return JSON.parse(tagsStr) } catch { return [] } + } const columns = [ - { title: '客户名称', dataIndex: 'name', key: 'name', render: (text: string, record: Customer) => { setSelectedCustomer(record); setDrawerOpen(true) }}>{text} }, + { title: '客户名称', dataIndex: 'name', key: 'name', render: (text: string, record: Customer) => ( + { setSelectedCustomer(record); setDrawerOpen(true) }}>{text} + )}, { title: '联系方式', key: 'contact', render: (_: unknown, record: Customer) => (
{record.phone &&
{record.phone}
} {record.email &&
{record.email}
}
)}, - { title: '标签', dataIndex: 'tags', key: 'tags', render: (tags: string[]) => ( - {tags.map(t => {t})} + { title: '标签', dataIndex: 'tags', key: 'tags', render: (tags: string) => ( + {parseTags(tags).map((t: string) => {t})} )}, - { title: '状态', dataIndex: 'status', key: 'status', render: (status: string) => }, - { title: '来源', dataIndex: 'source', key: 'source', render: (text: string) => {text} }, - { title: '对话次数', dataIndex: 'conversationCount', key: 'conversationCount', align: 'center' as const }, - { title: '满意度', dataIndex: 'satisfaction', key: 'satisfaction', align: 'center' as const, render: (v: number) => v > 0 ? {v.toFixed(1)} : - }, - { title: '待处理', dataIndex: 'pending', key: 'pending', align: 'center' as const, render: (v: number) => v > 0 ? : 0 }, - { title: '最近联系', dataIndex: 'lastContact', key: 'lastContact', render: (text: string) => {text} }, + { title: '状态', dataIndex: 'status', key: 'status', render: (s: string) => }, + { title: '来源', dataIndex: 'source', key: 'source', render: (t: string) => {t} }, + { title: '对话次数', dataIndex: 'conversation_count', key: 'conversation_count', align: 'center' as const }, + { title: '最近联系', dataIndex: 'last_contact_at', key: 'last_contact_at', render: (t: string) => {t || '-'} }, ] return (

客户管理

- +
- } placeholder="搜索客户名称、手机号、邮箱" value={search} onChange={e => setSearch(e.target.value)} className="w-64" allowClear /> - ({ value: v, label: statusMap[v].text }))} allowClear /> - } placeholder="搜索客户名称、手机号、邮箱" value={search} onChange={e => { setSearch(e.target.value); setPage(1) }} className="w-64" allowClear /> + } placeholder="搜索会话..." variant="borderless" /> +
+
{user?.nickname || '客服'}
+
在线
-
- {(['urgent', 'waiting', 'active'] as const).map(p => ( - - {priorityLabels[p]} - - ))} +
+ } placeholder="搜索会话..." variant="borderless" size="small" />
- {mockSessions.map(session => ( -
setSelectedId(session.id)} - > -
-
- - {session.name} -
-
- {session.unread > 0 && } - {session.time} + {sessions.length === 0 ? ( +
暂无会话
+ ) : ( + sessions.map(s => ( +
setSelectedId(s.id)} + > +
+
+ + 客户{s.customer_id} +
+ {new Date(s.created_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
+

+ +

-

{session.lastMessage}

-
- ))} + )) + )}
{/* 中间:聊天区 */}
- {selectedSession ? ( + {detail ? ( <>
- {selectedSession.name} + {detail.customerName}
- 转接 - 结束 + 转接 + 结束
-
-
- 你好,我想咨询一下订单物流问题 -
- 10:30 -
-
-
- 您好,请提供您的订单号,我帮您查询 -
- 10:31 -
-
-
- 订单号是:AB20260714001 -
- 10:31 -
-
-
- 好的,正在为您查询,请稍候... -
- 10:32 -
+ {detailLoading ? ( +
+ ) : detail.messages.length === 0 ? ( +
暂无消息
+ ) : ( + detail.messages.map((msg, i) => ( +
+
+ {msg.content} +
{msg.time}
+
+
+ )) + )}
setMessage(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter' && message.trim()) { - setMessage('') - } - }} + value={messageInput} + onChange={e => setMessageInput(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleSend() }} /> 😊 - 📎
) : ( -
- 选择一个会话开始接待 -
+
选择一个会话
)}
{/* 右侧:客户信息面板 */}
-
-
-
- + {detail ? ( +
+
+
+ +
+
+
{detail.customerName}
+
ID: {detail.id}
+
-
{selectedSession?.name || '访客'}
-
网页渠道
-
-
-
-
-
-
标签
-
- VIP客户 - 新客户 -
-
-
-
联系信息
-
-
📱 138****8888
-
📧 zhang***@example.com
-
-
-
-
统计数据
-
-
-
12
-
对话次数
+
标签
+
+ {detail.tags.map(t => {t})}
-
-
- 4.8 +
+
+
统计数据
+
+
+
{detail.conversationCount}
+
对话次数
+
+
+
+ {detail.satisfaction > 0 ? detail.satisfaction : '-'} {detail.satisfaction > 0 && } +
+
满意度
-
满意度
-
-
内部备注
-
- 之前咨询过售后问题... -
-
-
+ ) : null}
) } +function TagBadge({ status, priority }: { status: string; priority: string }) { + const color = status === 'ended' ? '#16a34a' : priorityColors[priority] || '#2563eb' + const text = status === 'ended' ? '已结束' : status === 'waiting' ? '等待中' : priorityLabels[priority] || status + return {text} +} + export default Dashboard