| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- // 管理配置和通知均由服务端保存,浏览器内存只保留当前身份的查询结果。
- export function createMerchantService(ctx) {
- ctx.data.merchantService ||= { conversationId: null, ticketId: null, ticket: null, messages: [], owner: 'AGENT', order: null };
- const identity = ctx.session.merchant;
- const scopeKey = `${identity.tenantId}:${identity.storeId}:${identity.actorId || identity.id}`;
- ctx.data.merchantState ||= {};
- ctx.data.merchantState[scopeKey] ||= { store: null, staff: [], channel: null, ruleDraft: null, rule: null, release: null, releaseState: null, notifications: [], unread: 0 };
- const state = ctx.data.merchantState[scopeKey];
- const shared = ctx.data.merchantService;
- const session = () => ctx.session.merchant;
- const requirePermission = permission => { if (!session()?.permissions.includes(permission)) throw new Error('当前账户无权执行此操作。'); };
- const store = () => {
- if (!session()?.storeId) throw new Error('当前账户没有授权店铺,请重新登录。');
- return session().storeId;
- };
- const request = (method, path, body, key) => ctx.api.request(method, path, body, { product: 'merchant', ...(key ? { idempotencyKey: key } : {}) });
- const command = (path, body) => request('POST', path, body);
- const saveTicket = ticket => { shared.ticket = ticket; shared.proposal = ticket; shared.ticketId = ticket.ticket_id; return ticket; };
- const base = () => `/admin/v1/stores/${store()}`;
- const mapRule = row => row ? { ...row, days: row.window_days, products: row.product_types.map(value => value === 'keyboard' ? '键盘' : value).join(','), active: !row.revoked } : null;
- async function rules() { const result = await request('GET', `${base()}/exchange-rules`); state.rule = mapRule(result.current); state.ruleDraft = mapRule(result.draft); return result; }
- async function refreshNotifications(unreadOnly = state.notificationUnreadOnly || false) {
- state.notificationUnreadOnly = unreadOnly;
- const result = await request('GET', `/admin/v1/notifications?${new URLSearchParams({store_id:store(),limit:'100',unread_only:String(unreadOnly)})}`);
- state.notifications = result.items; state.unread = result.unread_count; state.notificationCursor = result.next_cursor; state.notificationSnapshot = result.snapshot_id;
- return result;
- }
- return {
- state, shared, rules, refreshNotifications,
- async moreNotifications() { const result = await request('GET', `/admin/v1/notifications?${new URLSearchParams({store_id:store(),limit:'100',after:String(state.notificationCursor),unread_only:String(state.notificationUnreadOnly || false)})}`); state.notifications.push(...result.items); state.notificationCursor = result.next_cursor; state.unread = result.unread_count; state.notificationSnapshot = result.snapshot_id; },
- async markRead(id) { const item = await request('GET', `/admin/v1/notifications/${id}`); await refreshNotifications(); return item; },
- async markAllRead(snapshot) { await command('/admin/v1/notifications/read-all', { store_id: store(), through_id: snapshot }); await refreshNotifications(); },
- async access() { state.store = await request('GET', base()); let cursor; const staff = []; do { const result = await request('GET', `${base()}/staff${cursor ? `?cursor=${cursor}` : ''}`); staff.push(...result.items); cursor = result.next_cursor; } while (cursor); state.staff = staff; },
- async toggleStore() { state.store = await request('PUT', `${base()}/status`, { expected_version: state.store.version, enabled: !state.store.enabled }); },
- async toggleStaff(row) { await request('PUT', `${base()}/staff/${row.id}/status`, { expected_version: row.version, enabled: !row.enabled }); await this.access(); },
- async invite(form) { await command(`${base()}/staff/invitations`, { account: form.account, role: { '客服': 'staff', '主管': 'supervisor', '商家配置管理员': 'merchant' }[form.permission] }); await this.access(); },
- async channels() { const result = await request('GET', `${base()}/channels`); state.channel = result.channel; state.store = result.store; },
- async saveChannel(form) { state.channel = await request('PUT', `${base()}/channels`, { expected_version: state.channel?.version || 0, name: form.name, callback_url: form.callback, scope_confirmed: form.scope }); },
- async toggleChannel() { if (!state.channel) throw new Error('请先保存渠道配置。'); state.channel = await request('PUT', `${base()}/channels/status`, { expected_version: state.channel.version, enabled: !state.channel.enabled }); },
- async saveRule(form) { state.ruleDraft = mapRule(await request('PUT', `${base()}/exchange-rules/draft`, { expected_version: state.ruleDraft?.version || 0, base_version: state.rule?.version || 0, window_days: Number(form.days), product_types: form.products.split(/[,,]/).map(value => value.trim()).filter(Boolean).map(value => value === '键盘' ? 'keyboard' : value), citation: form.citation })); },
- async releaseState() { state.releaseState = await request('GET', `${base()}/agent-releases`); state.release = state.releaseState.current; },
- async release(form) { state.release = await command(`${base()}/agent-releases`, { expected_version: state.release?.version || 0, candidate_hash: state.releaseState.candidate_hash, regression_checked: form.releaseChecked, reason: form.releaseReason }); await this.releaseState(); },
- listConversations(cursor) { return request('GET', `/openapi/v1/conversations?${new URLSearchParams({ store_id: store(), limit: '20', ...(cursor ? { cursor } : {}) })}`); },
- async conversation(id = shared.conversationId) {
- if (!id) return null;
- const conversation = await request('GET', `/openapi/v1/conversations/${id}`);
- // 进入其他客户的会话时清理原订单与方案,只保留已经核对归属的上下文。
- const previousSubject = shared.conversation?.subject_id || shared.order?.subject_id;
- if ((previousSubject && previousSubject !== conversation.subject_id) || (shared.order && shared.order.subject_id !== conversation.subject_id) || (!previousSubject && shared.ticket)) { shared.order = null; shared.ticket = null; shared.ticketId = null; shared.proposal = null; shared.relatedTickets = []; }
- shared.conversationId = id; shared.conversation = conversation; shared.owner = conversation.control_owner;
- const events = await request('GET', `/openapi/v1/conversations/${id}/messages?limit=100`);
- shared.messages = events.items;
- shared.messageSequence = events.items.at(-1)?.sequence || 0;
- shared.hasMoreMessages = shared.messageSequence < events.sequence;
- return conversation;
- },
- async moreMessages() {
- const events = await request('GET', `/openapi/v1/conversations/${shared.conversationId}/messages?limit=100&after=${shared.messageSequence || 0}`);
- shared.messages.push(...events.items); shared.messageSequence = events.items.at(-1)?.sequence || shared.messageSequence;
- shared.hasMoreMessages = shared.messageSequence < events.sequence;
- },
- async control(action) {
- requirePermission('conversations:manage');
- const result = await command(`/openapi/v1/conversations/${shared.conversationId}/${action}`, { expected_control_version: shared.conversation.control_version });
- shared.conversation = result.payload; shared.owner = result.payload.control_owner; return result;
- },
- async reply(text) { requirePermission('conversations:reply'); return command(`/openapi/v1/conversations/${shared.conversationId}/messages`, { text, expected_control_version: shared.conversation.control_version }); },
- listOrders(cursor, subjectId) { return request('GET', `/openapi/v1/orders?${new URLSearchParams({ store_id: store(), limit: '20', ...(cursor ? { cursor } : {}), ...(subjectId ? { subject_id: subjectId } : {}) })}`); },
- async order(id) { const order = await request('GET', `/openapi/v1/orders/${id}`); if (session().role === 'staff' && shared.conversation && order.subject_id !== shared.conversation.subject_id) throw new Error('订单不属于当前会话客户。'); if (shared.ticket && shared.ticket.order_id !== id) { shared.ticket = null; shared.ticketId = null; shared.proposal = null; } shared.order = order; return order; },
- async proposal(quantity, reason) {
- requirePermission('after_sales:create');
- const current = shared.ticket;
- const control = { conversation_id: shared.conversationId, expected_control_version: shared.conversation.control_version };
- if (current && ['DRAFT', 'WAITING_REVIEW', 'NEEDS_REVISION'].includes(current.status)) return saveTicket(await command(`/openapi/v1/after-sales/proposals/${current.proposal_id}/revisions`, { expected_version: current.version, quantity, reason, ...control }));
- if (!shared.order) throw new Error('请先选择客户订单。');
- return saveTicket(await command('/openapi/v1/after-sales/proposals', { order_id: shared.order.order_id, item_id: shared.order.item_id, quantity, reason, ...control }));
- },
- async ticket(id = shared.ticketId) { return id ? saveTicket(await request('GET', `/openapi/v1/tickets/${id}`)) : null; },
- feedback: id => request('GET', `/openapi/v1/tickets/${id}/feedback`),
- tickets(cursor, subjectId) { return request('GET', `/openapi/v1/tickets?${new URLSearchParams({ store_id: store(), limit: '20', ...(cursor ? {cursor} : {}), ...(subjectId ? {subject_id:subjectId} : {}) })}`); },
- approvals(cursor) { return request('GET', `/openapi/v1/approvals?${new URLSearchParams({ store_id: store(), limit: '20', ...(cursor ? { cursor } : {}) })}`); },
- async decision(ticket, decision, reason) { requirePermission('approvals:decide'); return saveTicket(await command(`/openapi/v1/approvals/${ticket.approval_id}/decisions`, { expected_version: ticket.version, proposal_version: ticket.proposal_version, decision, reason })); },
- async publishRule() {
- const draft = state.ruleDraft;
- if (!draft) throw new Error('请先保存规则草稿。');
- const result = await command(`${base()}/exchange-rules/draft/publish`, { expected_version: draft.version, base_version: draft.base_version });
- await rules(); return result;
- },
- async revokeRule(reason) { const result = await command(`${base()}/exchange-rules/revoke`, { expected_version: state.rule.version, reason }); await rules(); return result; },
- knowledge(cursor) { return request('GET', `/admin/v1/stores/${store()}/knowledge?${new URLSearchParams({ limit: '20', ...(cursor ? { cursor } : {}) })}`); },
- document(id) { return request('GET', `/admin/v1/stores/${store()}/knowledge/${id}`); },
- saveDocument(document, form) { return request('PUT', `/admin/v1/stores/${store()}/knowledge/${document.document_id}`, { expected_version: document.version, title: form.title, text: form.text }); },
- transitionDocument(document, action) { return request('POST', `/admin/v1/stores/${store()}/knowledge/${document.document_id}/${action}`, { expected_version: document.version }); },
- billingAccount() { return request('GET', '/admin/v1/billing/account'); },
- billingRequests(after) { return request('GET', `/admin/v1/billing/requests${after ? `?after=${encodeURIComponent(after)}` : ''}`); },
- budget(account, amount, reason, id) { return request('PUT', `/admin/v1/billing/requests/${id}`, { kind: 'BUDGET', expected_version: account.policy_version, daily_limit: Math.round(Number(amount) * 1000000), reason }); },
- budgetDecision(item, approve, reason) { return request('POST', `/admin/v1/billing/requests/${item.id}/decisions`, { expected_version: item.version, approve, reason }); },
- };
- }
|