Files
websopy-mp/src/api/ticket.ts
赵忠林 f2d6b029f1 feat(api-keys): 优化隐私协议同意逻辑及样式
- 修正api-keys样式导入路径,改为developer路径
- 在开发者申请页面增加服务协议和隐私政策勾选框
- 提交时校验协议是否已勾选,未勾选时提示用户
- 优化申请页面协议区域样式及交互
- 在用户验证页面添加隐私政策同意勾选框
- 提交用户验证表单时检查协议同意状态
- 增加打开服务协议和隐私政策的链接跳转
- 更新项目配置,增加passport/webview路由配置
- 修复createTicket函数中日期格式正则表达式问题
2026-04-13 02:40:27 +08:00

297 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 工单/技术支持 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++;
}
}