From 9c36ab38d2f710fb93985fd32a1b5c2eb7a77065 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 10 Sep 2026 17:38:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B7=A5=E5=8D=95=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=9F=A5=E4=B8=8D=E5=88=B0+=E5=BE=85=E5=8A=9E=E6=B8=85?= =?UTF-8?q?=E4=B8=8D=E6=8E=89=20=E5=9B=9B=E4=B8=AA=E5=8F=A0=E5=8A=A0?= =?UTF-8?q?=E7=BC=BA=E9=99=B7=E6=A0=B9=E6=B2=BB(2026-09-10=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8A=A5=E9=9A=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 报障现象: owner端工单管理查不到任何工单, 但有个工单待办; 待办弹窗无处理按钮致待办数不变。 根因链(全部有DB/代码证据): 1. 弹窗缺客户动作按钮: 双身份用户(admin既是TK0004客户又是staff), staff视角在 staff_replied态一个按钮都没有, 而此刻待办要求的动作是客户确认——待办永远清不掉。 修: staff视角下当前用户=工单客户且状态在已回复态时追加客户动作按钮。 2. 管理页默认队列=待认领, 当时无待认领单+前端首次查询点击渲染时序中间态→看似查不到。 修: 默认队列改全部工单。 3. T12转派校验不全: 只查目标用户属于0机构不查owner角色, 测试时把TK0003转给无角色的hyz1 产生孤儿工单(角色池无人可认领+hyz1打开管理页被权限分支挡)。 修: 目标用户必须持owner角色否则TK_E004拒绝; 候选端点只列持owner角色的用户; 孤儿工单TK0003数据修复退回角色池。 4. staff_tickets的all分支权限过窄: 无owner角色的账号直接返回空, 受理人看不到自己手上的工单。 修: 无owner角色账号至少能看到受理人是自己的工单。 --- ticket/core.py | 25 +++++++++++++++++++------ wwwroot/api/ticket_detail_popup.dspy | 23 +++++++++++++++++++++++ wwwroot/manage/index.ui | 4 ++-- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/ticket/core.py b/ticket/core.py index 8f1e529..38ceba1 100644 --- a/ticket/core.py +++ b/ticket/core.py @@ -454,7 +454,12 @@ async def ticket_transfer(ticket_id, user_id, to_role='', to_user='', reason='') # 目标用户的角色(取 owner 类第一个作为工单角色,保持 I2/I3 语义) troles = await get_user_roles(sor, to_user) owner_roles = [r for r in troles if r.startswith('owner.')] - new_role = owner_roles[0] if owner_roles else cur_role + # 2026-09-10 修复:目标用户必须持有 owner 角色。原逻辑无角色时静默回退 + # cur_role,产生孤儿工单:角色池无人可认领(目标不持该角色), + # 目标自己打开管理页也被权限分支挡住——待办在却查无此单。 + if not owner_roles: + return False, ('TK_E004', '目标用户未持有 owner 组织类型角色,无法受理工单,请改派角色或先为其分配角色') + new_role = owner_roles[0] ok = await _cas_status(sor, ticket_id, S_HUMAN_PROCESSING, { 'status': S_HUMAN_PROCESSING, 'assignee_role': new_role, 'assignee_id': to_user}) @@ -496,8 +501,10 @@ async def transfer_candidates(): "ORDER BY name", {}) await sor.sqlExe("COMMIT", {}) urecs = await sor.sqlExe( - "SELECT id, username, nick_name FROM users WHERE orgid='0' AND user_status='0' " - "ORDER BY username", {}) + "SELECT DISTINCT u.id, u.username, u.nick_name FROM users u " + "JOIN userrole ur ON ur.userid=u.id JOIN role r ON r.id=ur.roleid " + "WHERE u.orgid='0' AND u.user_status='0' AND r.orgtypeid='owner' " + "AND r.name NOT IN ('*','customer') ORDER BY u.username", {}) await sor.sqlExe("COMMIT", {}) roles = [{'value': 'owner.' + str(getattr(r, 'name', '')), 'text': 'owner.' + str(getattr(r, 'name', ''))} for r in (rrecs or [])] @@ -554,9 +561,15 @@ async def list_staff_tickets(user_id, roles=None, queue='all', limit=100): "AND assignee_id=${u}$") params["u"] = str(user_id or '') else: - if not _is_admin(roles) and not any(r.startswith('owner.') for r in roles): - return [] - cond = "status NOT IN ('closed','cancelled') OR closed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)" + # all: admin/owner 角色看全量;其他账号至少能看到自己受理的工单 + # (2026-09-10 修复:受理人若无 owner 角色,原逻辑直接返回[], + # 受理人看不到自己手上的工单——待办点进来却查无此单) + if _is_admin(roles) or any(r.startswith('owner.') for r in roles): + cond = ("status NOT IN ('closed','cancelled') " + "OR closed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)") + else: + cond = "assignee_id=${u}$" + params["u"] = str(user_id or '') recs = await sor.sqlExe( "SELECT t.id, t.ticket_no, t.title, t.category, t.priority, t.status, " "t.assignee_role, t.assignee_id, t.customer_org_id, t.created_at, " diff --git a/wwwroot/api/ticket_detail_popup.dspy b/wwwroot/api/ticket_detail_popup.dspy index c93a6a9..94ac2fe 100644 --- a/wwwroot/api/ticket_detail_popup.dspy +++ b/wwwroot/api/ticket_detail_popup.dspy @@ -197,6 +197,29 @@ else: + "}]}]}}]};" "bricks.widgetBuild(desc,bricks.app);")}]}) +# 双身份用户(既是工单客户又是staff,如平台管理员自建工单): +# staff 视角在 agent_replied/staff_replied 态无任何按钮,但此刻待办要求的动作是 +# 「客户确认/追问」——缺按钮导致待办永远清不掉(2026-09-10 用户报障)。 +# 补:staff 视角下若当前用户就是该工单客户,追加客户动作按钮。 +if view == 'staff' and str(t.get('customer_user_id', '')) == str(user_id) \ + and status in ('agent_replied', 'staff_replied'): + buttons.append({ + "widgettype": "Button", + "options": {"label": "✔ 确认解决并关闭(客户动作)", "css": "primary"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", + "script": _post_js(confirm_url, {"ticket_id": ticket_id})}]}) + input_widgets.append({ + "widgettype": "UiText", "id": "tk_followup_input", + "options": {"name": "tk_followup_input", "placeholder": "问题没解决?在此输入追问内容…", + "width": "100%", "height": "70px"}}) + buttons.append({ + "widgettype": "Button", + "options": {"label": "追问(客户动作)", "css": "small"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", + "script": (_read_input_js('tk_followup_input') + + "if(!cv){new bricks.Message({title:'请填写追问内容',message:'追问内容不能为空'}).open();return;}" + + _post_js(followup_url, {"ticket_id": ticket_id, "content": "cv_placeholder"}))}]}) + # 追问/回复输入框 content 占位符替换为 JS 变量 cv for b in buttons: for bind in (b.get('binds') or []): diff --git a/wwwroot/manage/index.ui b/wwwroot/manage/index.ui index fc31bb2..034eb35 100644 --- a/wwwroot/manage/index.ui +++ b/wwwroot/manage/index.ui @@ -8,7 +8,7 @@ "options": { "css": "card", "padding": "8px", "submit_label": "查询", "fields": [ - {"name": "queue", "label": "队列", "uitype": "code", "value": "pending", "cwidth": 10, + {"name": "queue", "label": "队列", "uitype": "code", "value": "all", "cwidth": 10, "data": [ {"value": "pending", "text": "待认领(我的角色池)"}, {"value": "mine", "text": "我受理的"}, @@ -30,7 +30,7 @@ "width": "100%", "css": "card", "data_url": "{{entire_url('/ticket/api/staff_tickets.dspy')}}", "data_method": "GET", "page_rows": 20, - "data_params": {"queue": "pending"}, + "data_params": {"queue": "all"}, "toolbar": { "tools": [ {"name": "view_detail", "label": "查看详情/处理", "selected_row": true}