Skip to content

Commit

Permalink
feat: action log include unmodified replies
Browse files Browse the repository at this point in the history
  • Loading branch information
sdjdd committed Jan 31, 2024
1 parent 26e5664 commit 9651865
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 68 deletions.
20 changes: 6 additions & 14 deletions next/api/src/controller/customer-service-action-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
customerServiceActionLogService,
} from '@/service/customer-service-action-log';
import { Ticket } from '@/model/Ticket';
import { Reply } from '@/model/Reply';
import { User } from '@/model/User';
import { OpsLogResponse } from '@/response/ops-log';
import { ReplyResponse } from '@/response/reply';
Expand Down Expand Up @@ -43,13 +42,14 @@ export class CustomerServiceActionLogController {
});

const ticketIds = new Set<string>();
const replyIds = new Set<string>();
const userIds = new Set<string>();

for (const log of logs) {
switch (log.type) {
case CustomerServiceActionLogType.Reply:
replyIds.add(log.revision.replyId);
if (log.reply) {
ticketIds.add(log.reply.ticketId);
}
userIds.add(log.operatorId);
break;
case CustomerServiceActionLogType.OpsLog:
Expand All @@ -62,29 +62,22 @@ export class CustomerServiceActionLogController {
}
}

const replies = await Reply.getMany(Array.from(replyIds), { useMasterKey: true });

for (const reply of replies) {
ticketIds.add(reply.ticketId);
}

const tickets = await Ticket.getMany(Array.from(ticketIds), { useMasterKey: true });
const users = await User.getMany(Array.from(userIds), { useMasterKey: true });

const replyById = _.keyBy(replies, (r) => r.id);
const ticketById = _.keyBy(tickets, (t) => t.id);

const logResult = logs.map((log) => {
switch (log.type) {
case CustomerServiceActionLogType.Reply:
const reply = replyById[log.revision.replyId];
const ticket = reply && ticketById[reply.ticketId];
const ticket = log.reply && ticketById[log.reply.ticketId];
return {
id: log.id,
type: 'reply',
ticketId: ticket?.id,
operatorId: log.operatorId,
revision: new ReplyRevisionResponse(log.revision),
reply: log.reply && new ReplyResponse(log.reply),
revision: log.revision && new ReplyRevisionResponse(log.revision),
ts: log.ts.toISOString(),
};
case CustomerServiceActionLogType.OpsLog:
Expand All @@ -102,7 +95,6 @@ export class CustomerServiceActionLogController {
return {
logs: logResult,
tickets: tickets.map((ticket) => new TicketListItemResponse(ticket)),
replies: replies.map((reply) => new ReplyResponse(reply)),
users: users.map((user) => new UserResponse(user)),
};
}
Expand Down
53 changes: 51 additions & 2 deletions next/api/src/service/customer-service-action-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import _ from 'lodash';
import { OpsLog } from '@/model/OpsLog';
import { ReplyRevision } from '@/model/ReplyRevision';
import { User } from '@/model/User';
import { Reply } from '@/model/Reply';

export interface GetCustomerServiceActionLogsOptions {
from: Date;
Expand All @@ -23,7 +24,8 @@ export type CustomerServiceActionLog =
id: string;
type: CustomerServiceActionLogType.Reply;
operatorId: string;
revision: ReplyRevision;
reply?: Reply;
revision?: ReplyRevision;
ts: Date;
}
| {
Expand Down Expand Up @@ -129,6 +131,50 @@ export class CustomerServiceActionLogService {
async getLogs(options: GetCustomerServiceActionLogsOptions) {
const { limit = 10, desc } = options;

const replyReader = new BufferReader({
state: {
window: [options.from, options.to],
operatorIds: options.operatorIds,
desc: options.desc,
perCount: Math.min(200, limit),
},
read: async (state) => {
const query = Reply.queryBuilder()
.where('createdAt', '>=', state.window[0])
.where('createdAt', '<=', state.window[1])
.limit(state.perCount)
.orderBy('createdAt', state.desc ? 'desc' : 'asc');
if (state.operatorIds) {
const pointers = state.operatorIds.map(User.ptr.bind(User));
query.where('author', 'in', pointers);
}

const replies = await query.find({ useMasterKey: true });

if (replies.length) {
const last = replies[replies.length - 1];
if (state.desc) {
state.window[1] = subMilliseconds(last.createdAt, 1);
} else {
state.window[0] = addMilliseconds(last.createdAt, 1);
}
}

const value = replies.map<CustomerServiceActionLog>((reply) => ({
id: reply.id,
type: CustomerServiceActionLogType.Reply,
operatorId: reply.authorId,
reply,
ts: reply.createdAt,
}));

return {
value,
done: replies.length < state.perCount,
};
},
});

const replyRevisionReader = new BufferReader({
state: {
window: [options.from, options.to],
Expand All @@ -140,6 +186,8 @@ export class CustomerServiceActionLogService {
const query = ReplyRevision.queryBuilder()
.where('actionTime', '>=', state.window[0])
.where('actionTime', '<=', state.window[1])
.where('action', 'in', ['update', 'delete'])
.preload('reply')
.limit(state.perCount)
.orderBy('actionTime', state.desc ? 'desc' : 'asc');
if (state.operatorIds) {
Expand All @@ -162,6 +210,7 @@ export class CustomerServiceActionLogService {
id: rv.id,
type: CustomerServiceActionLogType.Reply,
operatorId: rv.operatorId,
reply: rv.reply,
revision: rv,
ts: rv.actionTime,
}));
Expand Down Expand Up @@ -220,7 +269,7 @@ export class CustomerServiceActionLogService {
});

const sortReader = new SortReader(
[opsLogReader, replyRevisionReader],
[replyReader, replyRevisionReader, opsLogReader],
desc ? (a, b) => b.ts.getTime() - a.ts.getTime() : (a, b) => a.ts.getTime() - b.ts.getTime()
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,15 @@ export function Exporter({ filters, open, onCancel }: ExporterProps) {

const collector = new ActionLogCollector<ExportRow>({
filters,
transform: ({ logs, tickets, replies, users }) => {
transform: ({ logs, tickets, users }) => {
const ticketById = keyBy(tickets, (t) => t.id);
const replyById = keyBy(replies, (r) => r.id);
const userById = keyBy(users, (u) => u.id);

const getReply = (id: string) => replyById[id];
const getAction = renderAction(getReply);
const getUser = (id: string) => userById[id];

return logs.map((log) => {
const row: ExportRow = {
ts: log.ts,
operatorName: getUser(log.operatorId)?.nickname,
action: getAction(log),
operatorName: userById[log.operatorId]?.nickname,
action: renderAction(log),
};
if (log.ticketId) {
const ticket = ticketById[log.ticketId];
Expand Down
17 changes: 6 additions & 11 deletions next/web/src/App/Admin/Stats/CustomerServiceAction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ function renderDetail(
) {
return (log: Log) => {
if (log.type === 'reply') {
const content = log.reply?.content ?? log.revision?.content;
return (
<Tooltip title={log.revision.content} placement="left">
<div className="max-w-[400px] truncate">{log.revision.content}</div>
<Tooltip title={content} placement="left">
<div className="max-w-[400px] truncate">{content}</div>
</Tooltip>
);
}
Expand Down Expand Up @@ -126,16 +127,10 @@ export function CustomerServiceAction() {
}
}, [data, pagination.desc]);

const [ticketById, replyById, userById] = useMemo(() => {
return [
keyBy(data?.tickets, (t) => t.id),
keyBy(data?.replies, (t) => t.id),
keyBy(data?.users, (t) => t.id),
];
const [ticketById, userById] = useMemo(() => {
return [keyBy(data?.tickets, (t) => t.id), keyBy(data?.users, (t) => t.id)];
}, [data]);

const getReply = (id: string) => replyById[id];

const getUserName = (id: string) => {
return userById[id]?.nickname ?? '未知';
};
Expand Down Expand Up @@ -230,7 +225,7 @@ export function CustomerServiceAction() {
{
key: 'action',
title: '操作',
render: renderAction(getReply),
render: renderAction,
},
{
key: 'detail',
Expand Down
63 changes: 32 additions & 31 deletions next/web/src/App/Admin/Stats/CustomerServiceAction/render.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
import { CustomerServiceActionLog } from '@/api/customer-service-action-log';
import { ReplySchema } from '@/api/reply';

export function renderAction(getReply: (id: string) => ReplySchema) {
return (log: CustomerServiceActionLog) => {
if (log.type === 'reply') {
const reply = getReply(log.revision.replyId);
const replyType = reply ? (reply.internal ? '内部回复' : '公开回复') : '回复';
switch (log.revision.action) {
case 'create':
return '创建' + replyType;
export function renderAction(log: CustomerServiceActionLog) {
if (log.type === 'reply') {
const { reply, revision } = log;
const replyType = reply ? (reply.internal ? '内部回复' : '公开回复') : '回复';
if (revision) {
switch (revision.action) {
case 'update':
return '修改' + replyType;
case 'delete':
return '删除' + replyType;
}
}
switch (log.opsLog.action) {
case 'changeAssignee':
return '修改负责人';
case 'changeCategory':
return '修改分类';
case 'changeFields':
return '修改自定义字段值';
case 'changeGroup':
return '修改客服组';
case 'close':
case 'reject':
case 'resolve':
return '关闭工单';
case 'reopen':
return '重新打开工单';
case 'replySoon':
return '稍后回复工单';
case 'replyWithNoContent':
return '认为工单无需回复';
default:
return log.opsLog.action;
if (reply) {
return '创建' + replyType;
}
};
return '';
}
switch (log.opsLog.action) {
case 'changeAssignee':
return '修改负责人';
case 'changeCategory':
return '修改分类';
case 'changeFields':
return '修改自定义字段值';
case 'changeGroup':
return '修改客服组';
case 'close':
case 'reject':
case 'resolve':
return '关闭工单';
case 'reopen':
return '重新打开工单';
case 'replySoon':
return '稍后回复工单';
case 'replyWithNoContent':
return '认为工单无需回复';
default:
return log.opsLog.action;
}
}
4 changes: 2 additions & 2 deletions next/web/src/api/customer-service-action-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type CustomerServiceActionLog =
type: 'reply';
ticketId?: string;
operatorId: string;
revision: ReplyRevision;
reply?: ReplySchema;
revision?: ReplyRevision;
ts: string;
}
| {
Expand All @@ -32,7 +33,6 @@ export interface GetCustomerServiceActionLogsOptions {
export interface GetCustomerServiceActionLogsResult {
logs: CustomerServiceActionLog[];
tickets: TicketSchema[];
replies: ReplySchema[];
users: UserSchema[];
}

Expand Down

0 comments on commit 9651865

Please sign in to comment.