diff --git a/next/api/src/router/ticket.ts b/next/api/src/router/ticket.ts index efab6912f..aac1de1dd 100644 --- a/next/api/src/router/ticket.ts +++ b/next/api/src/router/ticket.ts @@ -33,6 +33,7 @@ import { addInOrNotExistCondition } from '@/utils/conditions'; import { dynamicContentService } from '@/dynamic-content'; import { FileResponse } from '@/response/file'; import { File } from '@/model/File'; +import { lookupIp } from '@/utils/ip'; const router = new Router().use(auth); @@ -489,7 +490,7 @@ const extractSystemFields = ( }; }; -const { PERSIST_USERAGENT_INFO } = process.env; +const { PERSIST_USERAGENT_INFO, IP_LOOKUP_ENABLED } = process.env; router.post('/', async (ctx) => { const currentUser = ctx.state.currentUser as User; @@ -585,6 +586,26 @@ router.post('/', async (ctx) => { [...(customFields ?? []), ...builtInFields], 'field' ); + if (IP_LOOKUP_ENABLED) { + const ipField = fields.find(({ field }) => field === 'ip'); + const locationField = fields.find(({ field }) => field === 'location'); + const ispField = fields.find(({ field }) => field === 'isp'); + if (ipField && (!locationField || !ispField)) { + const { region, city, isp } = await lookupIp(ipField.value); + if (!locationField && region) { + fields.push({ + field: 'location', + value: `${region}${city ?? ''}`, + }); + } + if (!ispField && isp) { + fields.push({ + field: 'isp', + value: isp, + }); + } + } + } if (fields.length) { const ticketFieldIds = fields.map((field) => field.field); // TODO(sdjdd): Cache result @@ -1187,4 +1208,4 @@ router.post('/search-custom-field', customerServiceOnly, async (ctx) => { ctx.body = tickets.map((t) => new TicketListItemResponse(t)); }); -export default router; +export default router; \ No newline at end of file diff --git a/next/api/src/utils/ip.ts b/next/api/src/utils/ip.ts new file mode 100644 index 000000000..b2bc4cd56 --- /dev/null +++ b/next/api/src/utils/ip.ts @@ -0,0 +1,18 @@ +import axios from 'axios'; + +const { IP_LOOKUP_ENABLED, IP_LOOKUP_SERVER } = process.env; + +export async function lookupIp( + ip: string +): Promise<{ region?: string; city?: string; isp?: string }> { + if (!IP_LOOKUP_ENABLED) throw new Error('IP lookup is disabled.'); + if (!IP_LOOKUP_SERVER) throw new Error('IP lookup server is not configured.'); + + try { + const server = IP_LOOKUP_SERVER.replace(':ip', ip); + return (await axios.get(server)).data; + } catch (error) { + console.warn('Failed to lookup IP:', ip, error instanceof Error ? error.message : error, error); + return { region: undefined, city: undefined, isp: undefined }; + } +} diff --git a/resources/data/TicketField.jsonl b/resources/data/TicketField.jsonl index 5b1768df2..71dc35c1e 100644 --- a/resources/data/TicketField.jsonl +++ b/resources/data/TicketField.jsonl @@ -7,3 +7,5 @@ {"ACL":{},"objectId":"os","defaultLocale":"zh-cn","type":"text","title":"操作系统","active":true,"visible":false,"required":false,"meta":{"disableFilter": true}} {"ACL":{},"objectId":"os_name","defaultLocale":"zh-cn","type":"text","title":"操作系统名称","active":true,"visible":false,"required":false,"meta":{"disableFilter": true}} {"ACL":{},"objectId":"ip","defaultLocale":"zh-cn","type":"text","title":"IP","active":true,"visible":false,"required":false,"meta":{"disableFilter": true},"previewTemplate":"{{ value }}"} +{"ACL":{},"objectId":"location","defaultLocale":"zh-cn","type":"text","title":"地区","active":true,"visible":false,"required":false,"meta":{"disableFilter": true}} +{"ACL":{},"objectId":"isp","defaultLocale":"zh-cn","type":"text","title":"运营商","active":true,"visible":false,"required":false,"meta":{"disableFilter": true}} diff --git a/resources/data/TicketFieldVariant.jsonl b/resources/data/TicketFieldVariant.jsonl index 8ffe59a3c..abdc12431 100644 --- a/resources/data/TicketFieldVariant.jsonl +++ b/resources/data/TicketFieldVariant.jsonl @@ -7,3 +7,5 @@ {"description":"","titleForCustomerService":"操作系统","ACL":{},"locale":"zh-cn","field":{"__type":"Pointer","className":"TicketField","objectId":"os"},"title":"操作系统"} {"description":"","titleForCustomerService":"操作系统名称","ACL":{},"locale":"zh-cn","field":{"__type":"Pointer","className":"TicketField","objectId":"os_name"},"title":"操作系统名称"} {"description":"","titleForCustomerService":"IP","ACL":{},"locale":"zh-cn","field":{"__type":"Pointer","className":"TicketField","objectId":"ip"},"title":"IP"} +{"description":"","titleForCustomerService":"地区","ACL":{},"locale":"zh-cn","field":{"__type":"Pointer","className":"TicketField","objectId":"location"},"title":"地区"} +{"description":"","titleForCustomerService":"运营商","ACL":{},"locale":"zh-cn","field":{"__type":"Pointer","className":"TicketField","objectId":"isp"},"title":"运营商"}