工单与客服
本文面向带客服团队的站长与运营主管,讲清工单从提交到关闭的完整流转、权限与可见范围、自动预回复与超时关闭的行为边界,以及工单传递到上游供应商后的收发现状。文中字段名、状态值与菜单路径均取自当前代码;管理端页面为 frontend-admin-v3,「后台入口」指该端的实际路由。
1. 部门、分类与状态
工单部门固定四类,取值 sales、support、billing、abuse,页面标签依次为「销售」「技术支持」「财务」「投诉」。部门只是分类标签,不决定由谁处理——处理人靠指派。
优先级取值为 1 至 4,标签为「低」「中」「高」「紧急」;客户建单时未提交优先级则落库为 2(中)。工单传递到上游时优先级会被映射成 low / medium / high 三档:4 映射 high,3 与 2 都映射 medium,1 映射 low。
状态是会话推进的唯一依据,取值如下。
| 值 | 标签 | 由什么动作变成这个状态 |
|---|---|---|
| 0 | 开启 | 客户建单成功 |
| 1 | 客户回复 | 客户追加回复 |
| 2 | 员工回复 | 管理员回复;预回复命中时建单即为该状态 |
| 3 | 已关闭 | 管理员关闭、客户关闭或超时自动关闭 |
关闭原因存 close_reason,标签由系统给出:admin 管理员关闭、client 用户关闭、auto 自动关闭。重开成功会把它清空。
2. 权限与可见范围
工单相关的权限码共 5 个,全部必须显式授予;除下面标明隐含的部分外,ticket.manage 不隐含传递与预回复权限。
| 权限码 | 能做什么 | 后台入口 |
|---|---|---|
ticket.list | 看工单列表、工单详情、回复列表、上游投递状态与日志 | 工单处理 → 工单管理 → 工单列表 |
ticket.reply | 以员工身份回复工单、上传图片、撤回自己 2 分钟内的消息 | 工单交流页内操作 |
ticket.manage | 关闭、重开、指派处理人、重新注册上游回调 | 工单交流页内操作 |
ticket.delivery_manage | 工单传递规则、上游部门列表、上传防护配置 | 工单处理 → 工单管理 → 工单传递设置 |
ticket.pre_reply_manage | 工单预回复设置 | 工单处理 → 工单管理 → 工单预回复设置 |
ticket.manage 隐含 ticket.list 与 ticket.reply;系统内置默认角色拿不到 ticket.list 之外的任何工单权限,「访客」类角色只有 ticket.list。
可见范围的边界要记牢三点:
- 列表接口没有按部门或处理人过滤的能力,拿到
ticket.list的人看到的是全部工单。要限制某人只看自己负责的工单,只能靠不给他ticket.list。 - 客户邮箱在后续 API 响应里按权限码
privacy.view_raw脱敏成首字母***@域名;缺少该权限的账号看到的是掩码,无法据此联系客户。工单通知邮件本身不受影响,仍发往真实地址。 - 「指派处理人」下拉只列启用(
status=1)且持ticket.reply的管理员,一次最多返回 100 人;停用或没有回复权限的账号既不能出现在候选里,也不能被指派(接口会直接报「该管理员无工单处理权限」)。
3. 建单、回复与撤回
客户建单要求标题非空(最长 200 字),内容与图片至少有一项(内容最长 10000 字);图片单条消息最多 9 张,上传接口只接受 image/jpeg、image/png、image/webp,单张不超过 5 MB,落盘在 storage/app/private/tickets/,只能通过签名短链读取。工单可以关联一个属于该客户自己的服务实例,选了别的用户的服务会被拒绝。
管理端不提供「代客户建单」入口:工单只能由客户在会员中心提交。管理员在客户视角看到的建单能力只在服务控制台的报障入口(服务实例 → 提交工单),最终仍以该客户身份建单。
员工回复、客户回复共用同一条消息通道:正文可为空但必须有图片,正文按纯文本清洗入库。两条约定容易被忽略:
- 没有内部备注。 系统里不存在「仅内部可见」的回复类型,员工发出的每一条回复客户都能在会员中心看到,并会触发客户邮件与站内信。要写内部沟通记录,不要用工单回复。
- 撤回有时限。 只有发送者本人能撤回自己的消息,且距发送不超过 120 秒;撤回后正文清空、附件清空,只保留一条「消息已撤回」占位。客户或员工都无法撤回对方的消息。
客户回复会把状态置为「客户回复」;员工回复与上游回调会把状态置为「员工回复」。已关闭工单拒绝任何一侧继续回复,客户必须先重开。关闭与回复并发时以行锁内的先到者为准,后到的一方会收到「工单已关闭」。
4. 分配与转派
指派入口在工单交流页右栏的「处理人」下拉,保存后写 assignee_id,接口是 PUT /api/v2/admin/tickets/{ticket}/assignment,需要 ticket.manage。转派就是再指派一次:重新提交另一个管理员 ID 即可,不需要先解除。
指派会影响通知收件人范围,这是最容易出错的联动:
- 工单没有处理人时,客户建单与客户回复的提醒邮件会发给全部启用且持
ticket.reply、且填了邮箱的管理员(按邮箱去重,只发一次)。 - 工单有处理人、且该处理人本身在可接收名单里时,提醒只发给这一个人,其余客服收不到。所以指派既是分工,也是「静音开关」。
工单列表默认筛选是「进行中」(状态 0、1、2),支持按状态、优先级、部门、关键词(标题模糊或工单 ID 精确)过滤,每页默认 20 条、上限 100 条。
4.1 工单交流页与接口限流
工单交流页(/admin/ticket-conversations/{id})左右分栏:左栏是「沟通记录」与回复输入框,右栏是「提交用户、工单分类、优先级、处理人、关联服务、创建时间、关闭原因」的描述列表与处理人选择器;未关闭的工单才显示指派区。列表页点击标题行即跳进本页,工单 ID 与客户 ID 也支持从其他模块(如日志中心)跳转过来。
管理端接口的限流写在路由上,做批量操作前先知道这些数字,免得撞上「请求过于频繁」:
| 动作 | 接口 | 限流 |
|---|---|---|
| 管理端回复 | POST /api/v2/admin/tickets/{ticket}/replies | 未单独限流(需 ticket.reply) |
| 撤回消息 | POST /api/v2/admin/tickets/{ticket}/replies/{replyId}/recalls | 未单独限流 |
| 重开工单 | POST /api/v2/admin/tickets/{ticket}/reopen | 每 10 分钟 10 次 |
| 客户建单 / 回复 / 上传 / 关闭 | /api/v2/client/tickets、/tickets/{id}/replies、/tickets/upload-images、/tickets/{id}/closures | 每分钟 10 / 10 / 12 / 10 次 |
5. 预回复(自动首响)
预回复让客户建单成功的瞬间就收到一条员工回复,避免「提交后无人应答」。配置在「工单处理 → 工单管理 → 工单预回复设置」,权限码 ticket.pre_reply_manage,对应接口 GET/POST /api/v2/admin/ticket-pre-reply-settings,保存动作会写管理员操作日志(动作 ticket.pre_reply.update)。
| 字段 | 含义 | 约束与默认值 |
|---|---|---|
enabled | 是否启用预回复 | 必填;默认关闭,config/ticket_pre_reply.php 回退值为 false |
admin_user_id | 以谁的名义发这条回复 | 启用时必填,必须是启用且持 ticket.reply 的管理员 |
content | 普通工单的预回复正文 | 启用时清洗后不能为空,最长 5000 字 |
upstream_content | 命中传递规则、会推送到上游的工单专用正文 | 可选;留空时回退用 content,最长 5000 字 |
行为边界必须提前跟团队对齐:
- 预回复与建单在同一个事务里提交,只对新建工单生效一次,不是每次客户回复都触发。
- 它会立刻把工单状态改成「员工回复」(状态 2),也就是说自动关闭的倒计时从建单那一刻就开始走了(见下一节)。
- 它不转发上游:投递日志里会留一条
pre_reply_skipped记录。若工单命中传递规则,上游收到的是客户的首条内容,而不是预回复。 - 它不触发通知(客户不会因此收到一封「工单有新回复」的邮件)。
- 命不命中传递规则决定用哪份正文:
upstream_content留空时无需判断,一律用content。 - 配置残缺时保持静默:未启用、没选管理员、内容清洗后为空、管理员被停用或失去
ticket.reply,都不会生成预回复,也不会报错——只会在保存时由校验拦住前三种情况。
6. 超时自动关闭与催办
自动关闭由「自动化策略」页控制,入口是「系统设置 → 自动化策略」(直连 /admin/automation),权限 schedule.view,参数存 automation 配置组。
| 字段 | 含义 | 默认值 | 取值范围/说明 |
|---|---|---|---|
ticket_auto_close_enabled | 是否启用自动关闭 | 开启 | 关闭后工单只能人工关闭 |
ticket_auto_close_after_hours | 员工回复后多少小时关闭 | 48 | 1 至 720;代码内下限强制为 1 |
ticket_auto_close_schedule_mode | 任务执行周期 | 每小时 | 与「执行时间」一起决定运行时刻 |
ticket_auto_close_schedule_time | 任务执行时间 | 00:00:00 | 非每小时周期时用于对齐 |
判定口径只有一句话:扫描状态为「员工回复」(2)、且 updated_at 早于 now() - 小时数 的工单。由此推出四条实际后果:
- 「开启」和「客户回复」的工单永远不会被自动关闭,哪怕放了半年。
- 客户每次回复都会刷新
updated_at,倒计时重置;员工回复同理。 - 关闭时附件只做软删标记,文件不删;重开后历史附件重新可见。
- 关闭成功后才会发通知:邮件模板
100025(工单自动关闭通知)+ 站内信ticket_auto_closed。若工单已被别人抢先关掉,不会重复发信。
催办在关闭窗口过半时发一次,只发站内信、不发邮件:阈值取 ticket_auto_close_after_hours 的一半(向下取整,至少 1 小时),且尚未到关闭阈值。同一轮等待只提醒一次(幂等键为 idle_since: + 该轮起点时间戳),客户一旦回复、时间戳变化,下一轮重新计算。站内信类型 ticket_pending_reminder,标题「工单待您处理」。
任务本身注册在 ticket-auto-close(分类「工单管理」),执行记录在「日志中心 → 日志管理 → 自动任务日志」,也可在站内 storage/logs/laravel.log 搜 [定时任务] 工单自动关闭 核对关闭了哪些工单。
7. 上游工单传递与回调
传递的开关分三层,缺一层就整单不转发:一是关联服务所用产品必须绑定启用的 ZJMF 财务接口供应商;二是「工单传递设置」里必须有一条启用的规则命中该工单的部门与产品范围;三是标题与首条回复都不能命中该规则的屏蔽关键词。任一层不满足,工单仍在本站正常流转,只是不会出现在上游。
规则字段(接口 GET/POST/PUT/DELETE /api/v2/admin/ticket-delivery-rules,权限 ticket.delivery_manage):
| 字段 | 说明 |
|---|---|
name | 规则名称,最长 120 字 |
department | 命中哪个工单部门,四选一 |
supplier_id | 上游供应商,必须启用且配好启用的 ZJMF 财务接口绑定 |
provider_key | 前端不填,接口固定写成 ZJMF 财务接口 |
product_scope_mode | all 全部产品 / selected 指定产品 |
product_ids | selected 时至少一个,且每个产品都要有启用的 ZJMF 上游绑定 |
upstream_department_id | 上游部门 ID,从上游拉取后选择,最长 64 字符 |
enabled | 是否启用该规则 |
sync_admin_replies | 是否把管理员回复同步给上游;关闭则只转发客户消息 |
mask_keywords | 每行一个关键词,命中标题或首条回复即不转发,最长 10000 字 |
同一供应商 + 部门 + 产品范围模式只能有一条规则,重复创建会被拒绝。
转发与回调的实际行为:
- 工单投递成功后,后续历史回复会补投:该工单首条之后的所有回复会按发送者分别排队补发,上游不会丢中间的消息。
- 客户回复与管理员回复都会带前缀标识(
[下游用户消息]/[下游管理员消息]),上游可据此区分来源。 - 上游管理员回复经回调写入本站后会显示为
upstream_admin类型的员工消息,消息上的显示名取上游传入的admin_name,缺省显示「上游客服」。这类回调只写本地、不再回推上游,避免形成回环。 - 回调按事件 ID 幂等:同一个事件重复到达只记一条回复,投递日志里留
duplicate_event。回调附件限制为最多 9 张、单张不超过 5 MB、仅image/jpeg、image/png、image/webp。 - 回调签名校验失败、上游供应商被停用、找不到对应本地工单,都会写一条
failed投递日志并在响应里给出原因;密钥读到空值直接拒绝服务,不会降级放行。
排查入口都在工单交流页右栏的「上游传递」卡片:状态标签有「未配置 / 等待发送 / 发送中 / 已转发 / 转发失败 / 未转发」,另可看上游工单号、尝试次数、最近尝试时间、最近结果与投递日志分页。不转发时会给出原因码,常见的有 rule_not_matched(没有匹配的启用规则)、mask_keyword_matched(命中屏蔽关键词)、upstream_product_missing(服务未配上游产品)、supplier_binding_disabled(供应商接口绑定未启用)。若上游回调地址失效(换域名、改端口),用卡片上的「重新注册回调」按钮重发注册(接口 POST /api/v2/admin/tickets/{ticket}/upstream-delivery/callback-registration,需要 ticket.manage)。
8. 状态机:每一步能做什么
| 当前状态 | 客户可做 | 员工可做 |
|---|---|---|
| 0 开启 | 回复、关闭、上传图片 | 回复、指派、关闭、重开(重开会失败,仅已关闭可重开) |
| 1 客户回复 | 继续回复、关闭、上传图片 | 回复、指派、关闭 |
| 2 员工回复 | 回复(转为 1)、关闭、上传图片 | 回复、指派、关闭 |
| 3 已关闭 | 重开 | 重开 |
已关闭状态下,客户与员工的回复接口都会被拒绝,页面上也不提供回复框;「重开」是唯一出口,重开后状态回到「开启」、关闭原因清空、关闭期间软删的附件恢复可见,同时给客户写一条站内信 ticket_reopened。管理端重开有每 10 分钟 10 次的接口限流,客户侧重开同样受限。
9. 出问题先看这里
- 客户说没收到邮件:先确认建单/回复是否真的触发了通知(工单是否已有处理人,决定了收件人是全体客服还是单人),再去「日志中心 → 日志管理 → 邮件日志」按收件人与模板号查发送状态,见通知与站内信。
- 客服说看不到某个工单:工单列表不按人过滤,看不到就是缺
ticket.list,去「系统设置 → 权限管理」核对该角色权限。 - 指派下拉是空的:说明系统里没有启用且持
ticket.reply的管理员。下拉本身不依赖邮箱,但收件人缺邮箱时通知会静默失败——被指派人也收不到提醒。 - 工单没推送到上游:看「上游传递」卡片的最近结果与原因码,对照第 7 节的三种前置条件;上传类问题另查「工单传递设置」里的上传防护开关。
- 工单被自动关掉了:核对「自动化策略」里的自动关闭开关与小时数;判定只看状态 2 与
updated_at,与客户是否已读无关。