Skip to content

Commit

Permalink
feat: evaluation time limit (#944)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdjdd authored Nov 1, 2023
1 parent 8da6071 commit b0c2386
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 49 deletions.
3 changes: 2 additions & 1 deletion next/api/src/common/http/handler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getArguments, getParams } from '.';

const METADATA_KEY = Symbol('handlers');

export type HttpMethod = 'get' | 'post' | 'patch' | 'delete';
export type HttpMethod = 'get' | 'put' | 'post' | 'patch' | 'delete';

export interface Handler {
controllerMethod: string | symbol;
Expand All @@ -27,6 +27,7 @@ export function Handler(httpMethod: HttpMethod, path?: string) {
}

export const Get = (path?: string) => Handler('get', path);
export const Put = (path?: string) => Handler('put', path);
export const Post = (path?: string) => Handler('post', path);
export const Patch = (path?: string) => Handler('patch', path);
export const Delete = (path?: string) => Handler('delete', path);
Expand Down
46 changes: 46 additions & 0 deletions next/api/src/controller/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from 'zod';

import { BadRequestError, Body, Controller, Get, Param, Put, UseMiddlewares } from '@/common/http';
import { Config } from '@/config';
import { auth, customerServiceOnly } from '@/middleware';

const CONFIG_SCHEMAS: Record<string, z.Schema<any>> = {
weekday: z.array(z.number()),
work_time: z.object({
from: z.object({
hours: z.number(),
minutes: z.number(),
seconds: z.number(),
}),
to: z.object({
hours: z.number(),
minutes: z.number(),
seconds: z.number(),
}),
}),
evaluation: z.object({
timeLimit: z.number().int().min(0),
}),
};

@Controller('config')
@UseMiddlewares(auth, customerServiceOnly)
export class ConfigController {
@Get(':key')
getEvaluation(@Param('key') key: string) {
if (!(key in CONFIG_SCHEMAS)) {
throw new BadRequestError(`Invalid config key "${key}"`);
}
return Config.get(key);
}

@Put(':key')
async setEvaluation(@Param('key') key: string, @Body() body: any) {
const schema = CONFIG_SCHEMAS[key];
if (!schema) {
throw new BadRequestError(`Invalid config key "${key}"`);
}
const data = schema.parse(body);
await Config.set(key, data);
}
}
3 changes: 3 additions & 0 deletions next/api/src/model/Ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ export class Ticket extends Model {
@field()
channel?: string;

@field()
closedAt?: Date;

getUrlForEndUser() {
return `${config.host}/tickets/${this.nid}`;
}
Expand Down
24 changes: 0 additions & 24 deletions next/api/src/router/config.ts

This file was deleted.

2 changes: 0 additions & 2 deletions next/api/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import trigger from './trigger';
import timeTrigger from './time-trigger';
import reply from './reply';
import ticketStats from './ticket-stats';
import config from './config';

const router = new Router({ prefix: '/api/2' }).use(catchYupError, catchLCError, catchZodError);

Expand All @@ -23,7 +22,6 @@ router.use('/triggers', trigger.routes());
router.use('/time-triggers', timeTrigger.routes());
router.use('/replies', reply.routes());
router.use('/ticket-stats', ticketStats.routes());
router.use('/config', config.routes());

initControllers(router);

Expand Down
6 changes: 5 additions & 1 deletion next/api/src/router/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import AV from 'leancloud-storage';
import _ from 'lodash';
import UAParser from 'ua-parser-js';

import { config } from '@/config';
import { Config, config } from '@/config';
import * as yup from '@/utils/yup';
import { auth, customerServiceOnly, include, parseRange, sort } from '@/middleware';
import { Model, QueryBuilder } from '@/orm';
Expand Down Expand Up @@ -34,6 +34,7 @@ import { dynamicContentService } from '@/dynamic-content';
import { FileResponse } from '@/response/file';
import { File } from '@/model/File';
import { lookupIp } from '@/utils/ip';
import { ticketService } from '@/service/ticket';

const router = new Router().use(auth);

Expand Down Expand Up @@ -851,6 +852,9 @@ router.patch('/:id', async (ctx) => {
if (!config.allowModifyEvaluation && ticket.evaluation) {
return ctx.throw(409, 'Ticket is already evaluated');
}
if (!(await ticketService.isTicketEvaluable(ticket))) {
return ctx.throw(400, 'Sorry, you cannot create an evaluation in an expired ticket');
}
updater.setEvaluation({
...data.evaluation,
content: (
Expand Down
23 changes: 23 additions & 0 deletions next/api/src/service/ticket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { differenceInMilliseconds } from 'date-fns';

import { Ticket } from '@/model/Ticket';
import { Config } from '@/config';

export class TicketService {
async isTicketEvaluable(ticket: Ticket) {
if (!ticket.closedAt) {
return true;
}
const evaluationConfig = (await Config.get('evaluation')) as
| {
timeLimit: number;
}
| undefined;
if (!evaluationConfig || !evaluationConfig.timeLimit) {
return true;
}
return differenceInMilliseconds(new Date(), ticket.closedAt) <= evaluationConfig.timeLimit;
}
}

export const ticketService = new TicketService();
2 changes: 2 additions & 0 deletions next/api/src/ticket/TicketUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,11 @@ export class TicketUpdater {
break;
case 'close':
this.data.status = Status.CLOSED;
this.data.closedAt = new Date();
break;
case 'reopen':
this.data.status = Status.WAITING_CUSTOMER;
this.data.closedAt = null;
break;
}

Expand Down
41 changes: 41 additions & 0 deletions next/web/src/App/Admin/Settings/Evaluation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useConfig, useUpdateConfig } from '@/api/config';
import { LoadingCover } from '@/components/common';
import { Button, Form, InputNumber } from 'antd';

export function EvaluationConfig() {
const { data, isLoading } = useConfig<{ timeLimit: number }>('evaluation');

const { mutate, isLoading: isSaving } = useUpdateConfig('evaluation');

return (
<div className="p-10">
{isLoading && <LoadingCover />}
{!isLoading && (
<Form
layout="vertical"
initialValues={{
...data,
timeLimit: Math.floor((data?.timeLimit || 0) / 1000 / 60),
}}
onFinish={(data) =>
mutate({
...data,
timeLimit: data.timeLimit * 1000 * 60,
})
}
>
<Form.Item
name="timeLimit"
label="评价时限"
extra="工单关闭后多长时间内允许用户评价,设为 0 表示没有限制"
>
<InputNumber min={0} addonAfter="分钟" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={isSaving}>
保存
</Button>
</Form>
)}
</div>
);
}
4 changes: 2 additions & 2 deletions next/web/src/App/Admin/Settings/Others/Weekday.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ const WeekdayTime = () => {

export function Weekday() {
return (
<>
<div className="p-10">
<Weekdays />
<WeekdayTime />
</>
</div>
);
}
6 changes: 6 additions & 0 deletions next/web/src/App/Admin/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { EditTicketFormNoteTranslation } from './TicketFormNotes/EditTranslation
import { EditSupportEmail, NewSupportEmail, SupportEmailList } from './SupportEmails';
import { useCurrentUserIsAdmin } from '@/leancloud';
import { Result } from '@/components/antd';
import { EvaluationConfig } from './Evaluation';

const SettingRoutes = () => (
<Routes>
Expand Down Expand Up @@ -135,6 +136,7 @@ const SettingRoutes = () => (
<Route path="new" element={<NewSupportEmail />} />
<Route path=":id" element={<EditSupportEmail />} />
</Route>
<Route path="/evaluation" element={<EvaluationConfig />} />
</Routes>
);

Expand Down Expand Up @@ -243,6 +245,10 @@ const routeGroups: MenuDataItem[] = [
name: '工作时间',
path: 'weekday',
},
{
name: '评价',
path: 'evaluation',
},
],
},
];
Expand Down
45 changes: 26 additions & 19 deletions next/web/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
import { UseQueryOptions, useQuery, UseMutationOptions, useMutation, useQueryClient } from 'react-query';
import {
UseQueryOptions,
useQuery,
UseMutationOptions,
useMutation,
useQueryClient,
} from 'react-query';
import { http } from '@/leancloud';

async function fetchConfig<T>(key: string) {
const { data } = await http.get<T>(`/api/2/config/${key}`);
return data
return data;
}

async function updateConfig<T>(key: string, value: T) {
const { data } = await http.patch<T>(`/api/2/config/${key}`, value)
return data
const { data } = await http.put<T>(`/api/2/config/${key}`, value);
return data;
}

export const useConfig = <T>(key: string, options?: UseQueryOptions<T, Error>) => useQuery({
queryKey: ['config', key],
queryFn: () => fetchConfig<T>(key),
...options,
});
export const useConfig = <T>(key: string, options?: UseQueryOptions<T, Error>) =>
useQuery({
queryKey: ['config', key],
queryFn: () => fetchConfig<T>(key),
...options,
});

export const useUpdateConfig = <T>(key: string, options?: UseMutationOptions<T, Error, T>) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (value) => updateConfig(key, value).then(data => {
queryClient.setQueryData(['config', key], data);
return data
}),
mutationFn: (value) =>
updateConfig(key, value).then((data) => {
queryClient.setQueryData(['config', key], data);
return data;
}),
...options,
onSuccess: ((data, ...rest) => {
queryClient.setQueryData(['config', key], data);
onSuccess: (data, vars, ctx) => {
queryClient.setQueryData(['config', key], vars);
if (options?.onSuccess) {
options?.onSuccess(data, ...rest)
options?.onSuccess(data, vars, ctx);
}
})
},
});
}

};
7 changes: 7 additions & 0 deletions resources/schema/Ticket.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
"hidden": false,
"read_only": false
},
"closedAt": {
"type": "Date",
"v": 2,
"required": false,
"hidden": false,
"read_only": false
},
"tags": {
"type": "Array"
},
Expand Down

0 comments on commit b0c2386

Please sign in to comment.