fix: 工单管理查不到+待办清不掉 四个叠加缺陷根治(2026-09-10用户报障)

报障现象: 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角色账号至少能看到受理人是自己的工单。
This commit is contained in:
yumoqing 2026-09-10 17:38:31 +08:00
parent 86adf1aa8c
commit 9c36ab38d2
3 changed files with 44 additions and 8 deletions

View File

@ -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, "

View File

@ -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 []):

View File

@ -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}