- 修正api-keys样式导入路径,改为developer路径 - 在开发者申请页面增加服务协议和隐私政策勾选框 - 提交时校验协议是否已勾选,未勾选时提示用户 - 优化申请页面协议区域样式及交互 - 在用户验证页面添加隐私政策同意勾选框 - 提交用户验证表单时检查协议同意状态 - 增加打开服务协议和隐私政策的链接跳转 - 更新项目配置,增加passport/webview路由配置 - 修复createTicket函数中日期格式正则表达式问题
297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
/**
|
||
* 工单/技术支持 API
|
||
*/
|
||
import Taro from '@tarojs/taro';
|
||
import type { Ticket, TicketReply, TicketTemplate, TicketStats, FAQ } from '../types/ticket';
|
||
|
||
// 模拟数据
|
||
const mockTickets: Ticket[] = [
|
||
{
|
||
id: '1',
|
||
ticketNo: 'TK202604120001',
|
||
title: 'API 调用频率限制问题',
|
||
content: '我在使用批量接口时遇到了 429 错误,请问如何申请提高调用频率限制?',
|
||
type: 'technical',
|
||
priority: 'high',
|
||
status: 'processing',
|
||
category: 'api',
|
||
attachments: [],
|
||
creatorId: '1',
|
||
creatorName: '张三',
|
||
creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
|
||
assigneeId: 's1',
|
||
assigneeName: '技术支持小王',
|
||
assigneeAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka',
|
||
responseCount: 2,
|
||
solution: '已为您开通企业版调用配额',
|
||
createTime: '2026-04-12 10:30:00',
|
||
updateTime: '2026-04-12 14:20:00',
|
||
resolveTime: 230
|
||
},
|
||
{
|
||
id: '2',
|
||
ticketNo: 'TK202604110002',
|
||
title: '微信支付回调异常',
|
||
content: '支付完成后回调地址没有收到通知,请问如何排查问题?',
|
||
type: 'bug',
|
||
priority: 'urgent',
|
||
status: 'resolved',
|
||
category: 'payment',
|
||
attachments: ['/uploads/error-log.png'],
|
||
creatorId: '1',
|
||
creatorName: '张三',
|
||
creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
|
||
assigneeId: 's2',
|
||
assigneeName: '技术支持小李',
|
||
assigneeAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Bobby',
|
||
responseCount: 5,
|
||
solution: '回调地址需要配置白名单,已协助配置完成',
|
||
rating: 5,
|
||
feedback: '响应很快,问题解决了',
|
||
resolveTime: 180,
|
||
createTime: '2026-04-11 09:15:00',
|
||
updateTime: '2026-04-11 12:15:00',
|
||
resolveTime2: '2026-04-11 12:15:00'
|
||
},
|
||
{
|
||
id: '3',
|
||
ticketNo: 'TK202604100003',
|
||
title: '功能建议:支持 Webhook 重试机制',
|
||
content: '希望 Webhook 能够支持失败重试功能,提高消息可靠性',
|
||
type: 'feature',
|
||
priority: 'medium',
|
||
status: 'pending',
|
||
category: 'api',
|
||
attachments: [],
|
||
creatorId: '1',
|
||
creatorName: '张三',
|
||
creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
|
||
responseCount: 0,
|
||
createTime: '2026-04-10 16:45:00',
|
||
updateTime: '2026-04-10 16:45:00'
|
||
}
|
||
];
|
||
|
||
const mockReplies: TicketReply[] = [
|
||
{
|
||
id: 'r1',
|
||
ticketId: '1',
|
||
content: '您好,请问您的应用日调用量是多少?企业版默认配额为 10万次/天',
|
||
attachments: [],
|
||
isInternal: false,
|
||
senderId: 's1',
|
||
senderName: '技术支持小王',
|
||
senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka',
|
||
senderRole: 'support',
|
||
createTime: '2026-04-12 11:00:00'
|
||
},
|
||
{
|
||
id: 'r2',
|
||
ticketId: '1',
|
||
content: '我的日调用量大约在 15 万次左右',
|
||
attachments: [],
|
||
isInternal: false,
|
||
senderId: '1',
|
||
senderName: '张三',
|
||
senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
|
||
senderRole: 'user',
|
||
createTime: '2026-04-12 11:30:00'
|
||
},
|
||
{
|
||
id: 'r3',
|
||
ticketId: '1',
|
||
content: '已为您升级到企业高级版,配额提升至 50万次/天',
|
||
attachments: [],
|
||
isInternal: false,
|
||
senderId: 's1',
|
||
senderName: '技术支持小王',
|
||
senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka',
|
||
senderRole: 'support',
|
||
createTime: '2026-04-12 14:20:00'
|
||
}
|
||
];
|
||
|
||
const mockTemplates: TicketTemplate[] = [
|
||
{ id: 't1', title: 'API 调用问题', content: '【问题描述】\n\n【复现步骤】\n1.\n2.\n3.\n\n【错误信息】\n\n【环境信息】', type: 'technical', category: 'api', priority: 'medium' },
|
||
{ id: 't2', title: '支付相关问题', content: '【问题类型】\n□ 支付失败 □ 退款 □ 发票\n\n【订单号】\n\n【问题描述】', type: 'billing', category: 'payment', priority: 'high' },
|
||
{ id: 't3', title: 'Bug 反馈', content: '【Bug 标题】\n\n【影响范围】\n\n【复现步骤】\n1.\n2.\n\n【预期行为】\n\n【实际行为】', type: 'bug', category: 'other', priority: 'high' }
|
||
];
|
||
|
||
const mockFAQs: FAQ[] = [
|
||
{ id: 'f1', question: '如何获取 API Key?', answer: '登录控制台 -> 开发管理 -> API Key -> 创建 Key', category: 'api', viewCount: 1256, helpful: 234, notHelpful: 12, tags: ['API', '密钥'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-04-01' },
|
||
{ id: 'f2', question: '调用频率限制是多少?', answer: '免费版 1000次/天,个人版 1万次/天,企业版 10万次/天', category: 'api', viewCount: 2345, helpful: 456, notHelpful: 23, tags: ['限流', '配额'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-04-05' },
|
||
{ id: 'f3', question: '如何申请发票?', answer: '控制台 -> 财务 -> 发票管理 -> 申请发票', category: 'billing', viewCount: 1890, helpful: 345, notHelpful: 15, tags: ['发票', '财务'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-03-20' },
|
||
{ id: 'f4', question: 'Webhook 回调失败怎么办?', answer: '1. 检查回调地址是否可公网访问\n2. 确保返回 200 状态码\n3. 检查签名验证', category: 'api', viewCount: 1567, helpful: 289, notHelpful: 34, tags: ['Webhook', '回调'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-04-10' },
|
||
{ id: 'f5', question: '如何升级账户?', answer: '控制台 -> 套餐管理 -> 选择套餐 -> 在线支付', category: 'account', viewCount: 987, helpful: 178, notHelpful: 8, tags: ['升级', '套餐'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-03-15' }
|
||
];
|
||
|
||
/**
|
||
* 获取工单统计
|
||
*/
|
||
export async function getTicketStats(): Promise<TicketStats> {
|
||
return {
|
||
total: mockTickets.length,
|
||
pending: 1,
|
||
processing: 1,
|
||
resolved: 1,
|
||
avgResponseTime: 45,
|
||
avgResolveTime: 4.2,
|
||
satisfaction: 4.8
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 分页获取工单列表
|
||
*/
|
||
export async function pageTicket(params: {
|
||
status?: string;
|
||
type?: string;
|
||
priority?: string;
|
||
keyword?: string;
|
||
page?: number;
|
||
pageSize?: number;
|
||
}): Promise<{ list: Ticket[]; total: number }> {
|
||
const { status, type, priority, keyword, page = 1, pageSize = 10 } = params;
|
||
|
||
let filtered = [...mockTickets];
|
||
|
||
if (status) filtered = filtered.filter(t => t.status === status);
|
||
if (type) filtered = filtered.filter(t => t.type === type);
|
||
if (priority) filtered = filtered.filter(t => t.priority === priority);
|
||
if (keyword) {
|
||
const kw = keyword.toLowerCase();
|
||
filtered = filtered.filter(t => t.title.toLowerCase().includes(kw) || t.content.toLowerCase().includes(kw));
|
||
}
|
||
|
||
const start = (page - 1) * pageSize;
|
||
return { list: filtered.slice(start, start + pageSize), total: filtered.length };
|
||
}
|
||
|
||
/**
|
||
* 获取工单详情
|
||
*/
|
||
export async function getTicketDetail(id: string): Promise<Ticket | null> {
|
||
return mockTickets.find(t => t.id === id) || null;
|
||
}
|
||
|
||
/**
|
||
* 获取工单回复列表
|
||
*/
|
||
export async function listTicketReply(ticketId: string): Promise<TicketReply[]> {
|
||
return mockReplies.filter(r => r.ticketId === ticketId);
|
||
}
|
||
|
||
/**
|
||
* 创建工单
|
||
*/
|
||
export async function createTicket(data: Partial<Ticket>): Promise<Ticket> {
|
||
const newTicket: Ticket = {
|
||
id: String(Date.now()),
|
||
ticketNo: `TK${new Date().toISOString().replace(/[\-:T]/g, '').slice(0, 12)}`,
|
||
title: data.title || '',
|
||
content: data.content || '',
|
||
type: data.type || 'technical',
|
||
priority: data.priority || 'medium',
|
||
status: 'pending',
|
||
category: data.category || 'api',
|
||
attachments: data.attachments || [],
|
||
creatorId: '1',
|
||
creatorName: '张三',
|
||
creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
|
||
responseCount: 0,
|
||
createTime: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
||
updateTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||
};
|
||
|
||
mockTickets.unshift(newTicket);
|
||
return newTicket;
|
||
}
|
||
|
||
/**
|
||
* 回复工单
|
||
*/
|
||
export async function replyTicket(ticketId: string, content: string, attachments: string[] = []): Promise<TicketReply> {
|
||
const reply: TicketReply = {
|
||
id: String(Date.now()),
|
||
ticketId,
|
||
content,
|
||
attachments,
|
||
isInternal: false,
|
||
senderId: '1',
|
||
senderName: '张三',
|
||
senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
|
||
senderRole: 'user',
|
||
createTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||
};
|
||
|
||
mockReplies.push(reply);
|
||
|
||
const ticket = mockTickets.find(t => t.id === ticketId);
|
||
if (ticket) {
|
||
ticket.responseCount++;
|
||
ticket.updateTime = reply.createTime;
|
||
if (ticket.status === 'pending') ticket.status = 'processing';
|
||
}
|
||
|
||
return reply;
|
||
}
|
||
|
||
/**
|
||
* 关闭工单
|
||
*/
|
||
export async function closeTicket(id: string): Promise<void> {
|
||
const ticket = mockTickets.find(t => t.id === id);
|
||
if (ticket) ticket.status = 'closed';
|
||
}
|
||
|
||
/**
|
||
* 评价工单
|
||
*/
|
||
export async function rateTicket(id: string, rating: number, feedback: string): Promise<void> {
|
||
const ticket = mockTickets.find(t => t.id === id);
|
||
if (ticket) {
|
||
ticket.rating = rating;
|
||
ticket.feedback = feedback;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取工单模板
|
||
*/
|
||
export async function listTicketTemplate(): Promise<TicketTemplate[]> {
|
||
return mockTemplates;
|
||
}
|
||
|
||
/**
|
||
* 获取 FAQ 列表
|
||
*/
|
||
export async function pageFAQ(params: {
|
||
category?: string;
|
||
keyword?: string;
|
||
page?: number;
|
||
pageSize?: number;
|
||
}): Promise<{ list: FAQ[]; total: number }> {
|
||
const { category, keyword, page = 1, pageSize = 10 } = params;
|
||
|
||
let filtered = [...mockFAQs];
|
||
if (category) filtered = filtered.filter(f => f.category === category);
|
||
if (keyword) {
|
||
const kw = keyword.toLowerCase();
|
||
filtered = filtered.filter(f => f.question.toLowerCase().includes(kw) || f.answer.toLowerCase().includes(kw));
|
||
}
|
||
|
||
const start = (page - 1) * pageSize;
|
||
return { list: filtered.slice(start, start + pageSize), total: filtered.length };
|
||
}
|
||
|
||
/**
|
||
* FAQ 反馈
|
||
*/
|
||
export async function feedbackFAQ(id: string, helpful: boolean): Promise<void> {
|
||
const faq = mockFAQs.find(f => f.id === id);
|
||
if (faq) {
|
||
if (helpful) faq.helpful++;
|
||
else faq.notHelpful++;
|
||
}
|
||
}
|