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}