feat(developer): 完成小程序开发者中心和企业控制台改造
- 设计并实现了开发者中心与企业控制台两大模块 - 按用户角色区分开发者和企业客户,支持多项目类型及成员管理 - 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面 - 实现应用版本发布、消息通知中心、权限审批与开发者申请流程 - 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能 - 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块 - 优化后端API,支持已登录和未注册用户不同加入应用流程 - 前端按钮统一采用微信手机号授权,完善用户授权体验 - 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库 - 增加详细的类型定义文件,提升项目类型安全 - 新增超过55个页面及60个API接口,扩展应用功能和服务体系 - 完成全面的样式设计,实现一致的视觉风格和交互体验
This commit is contained in:
296
src/api/ticket.ts
Normal file
296
src/api/ticket.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 工单/技术支持 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++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user