fix: current_role是MariaDB保留字致CREATE INDEX/WHERE全1064——改名assignee_role/assignee_id
This commit is contained in:
parent
54f2f2d597
commit
fa415aa379
@ -17,8 +17,8 @@
|
|||||||
{"name": "status", "title": "工单状态", "type": "str", "length": 32, "nullable": "no", "default": "new"},
|
{"name": "status", "title": "工单状态", "type": "str", "length": 32, "nullable": "no", "default": "new"},
|
||||||
{"name": "customer_org_id", "title": "客户机构ID", "type": "str", "length": 32, "nullable": "no"},
|
{"name": "customer_org_id", "title": "客户机构ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "customer_user_id", "title": "客户用户ID", "type": "str", "length": 32, "nullable": "no"},
|
{"name": "customer_user_id", "title": "客户用户ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "current_role", "title": "当前受理角色", "type": "str", "length": 64},
|
{"name": "assignee_role", "title": "当前受理角色", "type": "str", "length": 64},
|
||||||
{"name": "current_assignee", "title": "当前受理人", "type": "str", "length": 32},
|
{"name": "assignee_id", "title": "当前受理人", "type": "str", "length": 32},
|
||||||
{"name": "processing_owner", "title": "agent处理锁标记", "type": "str", "length": 64},
|
{"name": "processing_owner", "title": "agent处理锁标记", "type": "str", "length": 64},
|
||||||
{"name": "followup_count", "title": "客户追问次数", "type": "int", "default": "0"},
|
{"name": "followup_count", "title": "客户追问次数", "type": "int", "default": "0"},
|
||||||
{"name": "agent_fail_rounds", "title": "agent连续失败轮数", "type": "int", "default": "0"},
|
{"name": "agent_fail_rounds", "title": "agent连续失败轮数", "type": "int", "default": "0"},
|
||||||
@ -31,8 +31,8 @@
|
|||||||
{"name": "uk_tk_no", "idxtype": "unique", "idxfields": ["ticket_no"]},
|
{"name": "uk_tk_no", "idxtype": "unique", "idxfields": ["ticket_no"]},
|
||||||
{"name": "idx_tk_cust", "idxtype": "index", "idxfields": ["customer_org_id", "status"]},
|
{"name": "idx_tk_cust", "idxtype": "index", "idxfields": ["customer_org_id", "status"]},
|
||||||
{"name": "idx_tk_status", "idxtype": "index", "idxfields": ["status"]},
|
{"name": "idx_tk_status", "idxtype": "index", "idxfields": ["status"]},
|
||||||
{"name": "idx_tk_role_status", "idxtype": "index", "idxfields": ["current_role", "status"]},
|
{"name": "idx_tk_role_status", "idxtype": "index", "idxfields": ["assignee_role", "status"]},
|
||||||
{"name": "idx_tk_assignee", "idxtype": "index", "idxfields": ["current_assignee", "status"]}
|
{"name": "idx_tk_assignee", "idxtype": "index", "idxfields": ["assignee_id", "status"]}
|
||||||
],
|
],
|
||||||
"codes": [
|
"codes": [
|
||||||
{"field": "category", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='tk_category'"},
|
{"field": "category", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='tk_category'"},
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
⚠ staff 动作端点(claim/staff_reply/transfer/transfer_candidates/staff_tickets)
|
⚠ staff 动作端点(claim/staff_reply/transfer/transfer_candidates/staff_tickets)
|
||||||
只能授 logined 而不能授固定角色——D4 决策:转派目标是 owner 组织类型下的
|
只能授 logined 而不能授固定角色——D4 决策:转派目标是 owner 组织类型下的
|
||||||
**任意动态角色**(role 表运行时可增删),RBAC 路径权限无法枚举动态角色,
|
**任意动态角色**(role 表运行时可增删),RBAC 路径权限无法枚举动态角色,
|
||||||
业务权限由服务端 _check_staff_access 校验(操作者须持有工单 current_role,
|
业务权限由服务端 _check_staff_access 校验(操作者须持有工单 assignee_role,
|
||||||
admin/superuser 豁免)。客户侧数据由 I5 服务端自过滤(customer_user_id/org_id)。
|
admin/superuser 豁免)。客户侧数据由 I5 服务端自过滤(customer_user_id/org_id)。
|
||||||
- owner.maintainer / owner.admin / owner.superuser:管理页壳 /ticket/manage/**。
|
- owner.maintainer / owner.admin / owner.superuser:管理页壳 /ticket/manage/**。
|
||||||
菜单「工单管理」入口也按角色 Jinja 分支渲染,客户直接访问 URL 403。
|
菜单「工单管理」入口也按角色 Jinja 分支渲染,客户直接访问 URL 403。
|
||||||
@ -42,7 +42,7 @@ PATHS_LOGINED = [
|
|||||||
f"/{MOD}/api/ticket_followup.dspy",
|
f"/{MOD}/api/ticket_followup.dspy",
|
||||||
f"/{MOD}/api/ticket_confirm.dspy",
|
f"/{MOD}/api/ticket_confirm.dspy",
|
||||||
f"/{MOD}/api/ticket_cancel.dspy",
|
f"/{MOD}/api/ticket_cancel.dspy",
|
||||||
# staff 侧 API(服务端 _check_staff_access 按工单 current_role 动态校验)
|
# staff 侧 API(服务端 _check_staff_access 按工单 assignee_role 动态校验)
|
||||||
f"/{MOD}/api/staff_tickets.dspy",
|
f"/{MOD}/api/staff_tickets.dspy",
|
||||||
f"/{MOD}/api/ticket_claim.dspy",
|
f"/{MOD}/api/ticket_claim.dspy",
|
||||||
f"/{MOD}/api/ticket_staff_reply.dspy",
|
f"/{MOD}/api/ticket_staff_reply.dspy",
|
||||||
|
|||||||
@ -226,8 +226,8 @@ async def process_one_ticket(ticket_id):
|
|||||||
# 基础设施故障(T4):fail_rounds+1 回退 new;超限转人工
|
# 基础设施故障(T4):fail_rounds+1 回退 new;超限转人工
|
||||||
if fail_rounds + 1 >= fail_max:
|
if fail_rounds + 1 >= fail_max:
|
||||||
moved = await core._cas_status(sor, ticket_id, core.S_AGENT_PROCESSING, {
|
moved = await core._cas_status(sor, ticket_id, core.S_AGENT_PROCESSING, {
|
||||||
'status': core.S_HUMAN_PENDING, 'current_role': default_role,
|
'status': core.S_HUMAN_PENDING, 'assignee_role': default_role,
|
||||||
'current_assignee': None, 'processing_owner': None,
|
'assignee_id': None, 'processing_owner': None,
|
||||||
'agent_fail_rounds': fail_rounds + 1})
|
'agent_fail_rounds': fail_rounds + 1})
|
||||||
if moved:
|
if moved:
|
||||||
await core._add_message(
|
await core._add_message(
|
||||||
@ -263,8 +263,8 @@ async def process_one_ticket(ticket_id):
|
|||||||
|
|
||||||
# T3:答不了 → 转人工
|
# T3:答不了 → 转人工
|
||||||
moved = await core._cas_status(sor, ticket_id, core.S_AGENT_PROCESSING, {
|
moved = await core._cas_status(sor, ticket_id, core.S_AGENT_PROCESSING, {
|
||||||
'status': core.S_HUMAN_PENDING, 'current_role': default_role,
|
'status': core.S_HUMAN_PENDING, 'assignee_role': default_role,
|
||||||
'current_assignee': None, 'processing_owner': None, 'agent_fail_rounds': 0})
|
'assignee_id': None, 'processing_owner': None, 'agent_fail_rounds': 0})
|
||||||
if moved:
|
if moved:
|
||||||
await core._add_message(
|
await core._add_message(
|
||||||
sor, ticket_id, 'agent', core.AGENT_USER, '',
|
sor, ticket_id, 'agent', core.AGENT_USER, '',
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
"""ticket/core.py — 工单状态机与全部迁移动作。
|
"""ticket/core.py — 工单状态机与全部迁移动作。
|
||||||
|
|
||||||
不变量(设计文档 §4.3):
|
不变量(设计文档 §4.3):
|
||||||
- I1 处理方唯一:status+current_role+current_assignee 联合表达;迁移全部乐观锁
|
- I1 处理方唯一:status+assignee_role+assignee_id 联合表达;迁移全部乐观锁
|
||||||
(UPDATE ... WHERE status=前置态 + SELECT 验证,照 pipeline_service claim 范式)
|
(UPDATE ... WHERE status=前置态 + SELECT 验证,照 pipeline_service claim 范式)
|
||||||
- I2 human_pending ⟺ current_role 非空 AND current_assignee NULL
|
- I2 human_pending ⟺ assignee_role 非空 AND assignee_id NULL
|
||||||
- I3 human_processing/staff_replied ⟹ current_assignee 非空
|
- I3 human_processing/staff_replied ⟹ assignee_id 非空
|
||||||
- I4 受理方每次变更必写 tk_transfers(同事务)
|
- I4 受理方每次变更必写 tk_transfers(同事务)
|
||||||
- I5 客户只见自己工单 + visibility='customer' 消息
|
- I5 客户只见自己工单 + visibility='customer' 消息
|
||||||
- I6 终态(closed/cancelled)不接受任何迁移
|
- I6 终态(closed/cancelled)不接受任何迁移
|
||||||
@ -215,8 +215,8 @@ async def create_ticket(user_id, org_id, title, description, category='other',
|
|||||||
'status': S_NEW,
|
'status': S_NEW,
|
||||||
'customer_org_id': str(org_id or ''),
|
'customer_org_id': str(org_id or ''),
|
||||||
'customer_user_id': str(user_id),
|
'customer_user_id': str(user_id),
|
||||||
'current_role': None,
|
'assignee_role': None,
|
||||||
'current_assignee': None,
|
'assignee_id': None,
|
||||||
'processing_owner': None,
|
'processing_owner': None,
|
||||||
'followup_count': 0,
|
'followup_count': 0,
|
||||||
'agent_fail_rounds': 0,
|
'agent_fail_rounds': 0,
|
||||||
@ -281,8 +281,8 @@ async def ticket_followup(ticket_id, user_id, org_id, content):
|
|||||||
if cur > followup_max:
|
if cur > followup_max:
|
||||||
default_role = await _get_param(sor, 'ticket_default_role', 'owner.maintainer')
|
default_role = await _get_param(sor, 'ticket_default_role', 'owner.maintainer')
|
||||||
ok = await _cas_status(sor, ticket_id, S_AGENT_REPLIED, {
|
ok = await _cas_status(sor, ticket_id, S_AGENT_REPLIED, {
|
||||||
'status': S_HUMAN_PENDING, 'current_role': default_role,
|
'status': S_HUMAN_PENDING, 'assignee_role': default_role,
|
||||||
'current_assignee': None, 'followup_count': cur})
|
'assignee_id': None, 'followup_count': cur})
|
||||||
if ok:
|
if ok:
|
||||||
await _add_message(sor, ticket_id, 'agent', AGENT_USER, '',
|
await _add_message(sor, ticket_id, 'agent', AGENT_USER, '',
|
||||||
'客户追问超过 %d 次,自动转人工处理。' % followup_max,
|
'客户追问超过 %d 次,自动转人工处理。' % followup_max,
|
||||||
@ -317,7 +317,7 @@ async def ticket_confirm(ticket_id, user_id, org_id):
|
|||||||
return False, ('TK_E002', '工单当前状态为 %s,不能确认关闭(需已有回复)' % status)
|
return False, ('TK_E002', '工单当前状态为 %s,不能确认关闭(需已有回复)' % status)
|
||||||
ok = await _cas_status(sor, ticket_id, status, {
|
ok = await _cas_status(sor, ticket_id, status, {
|
||||||
'status': S_CLOSED, 'closed_at': _now(), 'close_reason': 'resolved',
|
'status': S_CLOSED, 'closed_at': _now(), 'close_reason': 'resolved',
|
||||||
'current_role': None, 'current_assignee': None})
|
'assignee_role': None, 'assignee_id': None})
|
||||||
if not ok:
|
if not ok:
|
||||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||||
await _add_message(sor, ticket_id, 'customer', user_id, '',
|
await _add_message(sor, ticket_id, 'customer', user_id, '',
|
||||||
@ -341,7 +341,7 @@ async def ticket_cancel(ticket_id, user_id, org_id):
|
|||||||
return False, ('TK_E002', '智能助手正在处理中,请稍后再取消')
|
return False, ('TK_E002', '智能助手正在处理中,请稍后再取消')
|
||||||
ok = await _cas_status(sor, ticket_id, status, {
|
ok = await _cas_status(sor, ticket_id, status, {
|
||||||
'status': S_CANCELLED, 'closed_at': _now(), 'close_reason': 'cancelled',
|
'status': S_CANCELLED, 'closed_at': _now(), 'close_reason': 'cancelled',
|
||||||
'current_role': None, 'current_assignee': None})
|
'assignee_role': None, 'assignee_id': None})
|
||||||
if not ok:
|
if not ok:
|
||||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||||
await _add_message(sor, ticket_id, 'customer', user_id, '', '客户取消工单。')
|
await _add_message(sor, ticket_id, 'customer', user_id, '', '客户取消工单。')
|
||||||
@ -351,10 +351,10 @@ async def ticket_cancel(ticket_id, user_id, org_id):
|
|||||||
# ══════════════════ 运维动作 ══════════════════
|
# ══════════════════ 运维动作 ══════════════════
|
||||||
|
|
||||||
async def _check_staff_access(sor, t, user_id, roles):
|
async def _check_staff_access(sor, t, user_id, roles):
|
||||||
"""运维侧访问:admin/superuser 豁免;否则须持有工单 current_role。"""
|
"""运维侧访问:admin/superuser 豁免;否则须持有工单 assignee_role。"""
|
||||||
if _is_admin(roles):
|
if _is_admin(roles):
|
||||||
return True
|
return True
|
||||||
cur_role = str(getattr(t, 'current_role', '') or '')
|
cur_role = str(getattr(t, 'assignee_role', '') or '')
|
||||||
return bool(cur_role) and cur_role in roles
|
return bool(cur_role) and cur_role in roles
|
||||||
|
|
||||||
|
|
||||||
@ -368,9 +368,9 @@ async def ticket_claim(ticket_id, user_id):
|
|||||||
return False, E_NOT_FOUND
|
return False, E_NOT_FOUND
|
||||||
if not await _check_staff_access(sor, t, user_id, roles):
|
if not await _check_staff_access(sor, t, user_id, roles):
|
||||||
return False, ('TK_E001', '您不是该工单的受理角色,无法认领')
|
return False, ('TK_E001', '您不是该工单的受理角色,无法认领')
|
||||||
cur_role = str(getattr(t, 'current_role', '') or '')
|
cur_role = str(getattr(t, 'assignee_role', '') or '')
|
||||||
ok = await _cas_status(sor, ticket_id, S_HUMAN_PENDING, {
|
ok = await _cas_status(sor, ticket_id, S_HUMAN_PENDING, {
|
||||||
'status': S_HUMAN_PROCESSING, 'current_assignee': user_id})
|
'status': S_HUMAN_PROCESSING, 'assignee_id': user_id})
|
||||||
if not ok:
|
if not ok:
|
||||||
return False, ('TK_E005', '该工单刚被他人认领,请刷新队列')
|
return False, ('TK_E005', '该工单刚被他人认领,请刷新队列')
|
||||||
await _add_transfer(sor, ticket_id, 'claim', from_role=cur_role,
|
await _add_transfer(sor, ticket_id, 'claim', from_role=cur_role,
|
||||||
@ -391,7 +391,7 @@ async def ticket_staff_reply(ticket_id, user_id, content, internal=False):
|
|||||||
t = await _load_ticket(sor, ticket_id)
|
t = await _load_ticket(sor, ticket_id)
|
||||||
if not t:
|
if not t:
|
||||||
return False, E_NOT_FOUND
|
return False, E_NOT_FOUND
|
||||||
cur_assignee = str(getattr(t, 'current_assignee', '') or '')
|
cur_assignee = str(getattr(t, 'assignee_id', '') or '')
|
||||||
if not (_is_admin(roles) or cur_assignee == str(user_id)):
|
if not (_is_admin(roles) or cur_assignee == str(user_id)):
|
||||||
return False, ('TK_E001', '只有当前受理人可以回复该工单')
|
return False, ('TK_E001', '只有当前受理人可以回复该工单')
|
||||||
status = str(getattr(t, 'status', ''))
|
status = str(getattr(t, 'status', ''))
|
||||||
@ -440,8 +440,8 @@ async def ticket_transfer(ticket_id, user_id, to_role='', to_user='', reason='')
|
|||||||
status = str(getattr(t, 'status', ''))
|
status = str(getattr(t, 'status', ''))
|
||||||
if status != S_HUMAN_PROCESSING:
|
if status != S_HUMAN_PROCESSING:
|
||||||
return False, ('TK_E002', '工单当前状态为 %s,不能转派(需认领后 human_processing)' % status)
|
return False, ('TK_E002', '工单当前状态为 %s,不能转派(需认领后 human_processing)' % status)
|
||||||
cur_role = str(getattr(t, 'current_role', '') or '')
|
cur_role = str(getattr(t, 'assignee_role', '') or '')
|
||||||
cur_assignee = str(getattr(t, 'current_assignee', '') or '')
|
cur_assignee = str(getattr(t, 'assignee_id', '') or '')
|
||||||
|
|
||||||
if to_user:
|
if to_user:
|
||||||
# T12 转具体人:校验目标 ∈ '0' 机构用户
|
# T12 转具体人:校验目标 ∈ '0' 机构用户
|
||||||
@ -456,8 +456,8 @@ async def ticket_transfer(ticket_id, user_id, to_role='', to_user='', reason='')
|
|||||||
owner_roles = [r for r in troles if r.startswith('owner.')]
|
owner_roles = [r for r in troles if r.startswith('owner.')]
|
||||||
new_role = owner_roles[0] if owner_roles else cur_role
|
new_role = owner_roles[0] if owner_roles else cur_role
|
||||||
ok = await _cas_status(sor, ticket_id, S_HUMAN_PROCESSING, {
|
ok = await _cas_status(sor, ticket_id, S_HUMAN_PROCESSING, {
|
||||||
'status': S_HUMAN_PROCESSING, 'current_role': new_role,
|
'status': S_HUMAN_PROCESSING, 'assignee_role': new_role,
|
||||||
'current_assignee': to_user})
|
'assignee_id': to_user})
|
||||||
if not ok:
|
if not ok:
|
||||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||||
await _add_transfer(sor, ticket_id, 'transfer_user',
|
await _add_transfer(sor, ticket_id, 'transfer_user',
|
||||||
@ -476,8 +476,8 @@ async def ticket_transfer(ticket_id, user_id, to_role='', to_user='', reason='')
|
|||||||
return False, ('TK_E003', '目标角色不在 owner 组织类型角色列表中,请重新选择')
|
return False, ('TK_E003', '目标角色不在 owner 组织类型角色列表中,请重新选择')
|
||||||
full_role = to_role if '.' in to_role else 'owner.' + to_role
|
full_role = to_role if '.' in to_role else 'owner.' + to_role
|
||||||
ok = await _cas_status(sor, ticket_id, S_HUMAN_PROCESSING, {
|
ok = await _cas_status(sor, ticket_id, S_HUMAN_PROCESSING, {
|
||||||
'status': S_HUMAN_PENDING, 'current_role': full_role,
|
'status': S_HUMAN_PENDING, 'assignee_role': full_role,
|
||||||
'current_assignee': None})
|
'assignee_id': None})
|
||||||
if not ok:
|
if not ok:
|
||||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||||
await _add_transfer(sor, ticket_id, 'transfer_role',
|
await _add_transfer(sor, ticket_id, 'transfer_role',
|
||||||
@ -527,7 +527,7 @@ async def list_customer_tickets(user_id, org_id, roles=None, status='', limit=10
|
|||||||
params["st"] = status
|
params["st"] = status
|
||||||
wsql = (' WHERE ' + ' AND '.join(where)) if where else ''
|
wsql = (' WHERE ' + ' AND '.join(where)) if where else ''
|
||||||
recs = await sor.sqlExe(
|
recs = await sor.sqlExe(
|
||||||
"SELECT id, ticket_no, title, category, priority, status, current_role, "
|
"SELECT id, ticket_no, title, category, priority, status, assignee_role, "
|
||||||
"followup_count, closed_at, close_reason, created_at, updated_at "
|
"followup_count, closed_at, close_reason, created_at, updated_at "
|
||||||
"FROM tk_tickets" + wsql + " ORDER BY created_at DESC LIMIT ${lim}$", params)
|
"FROM tk_tickets" + wsql + " ORDER BY created_at DESC LIMIT ${lim}$", params)
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
@ -548,10 +548,10 @@ async def list_staff_tickets(user_id, roles=None, queue='all', limit=100):
|
|||||||
ph = ','.join("'role%d'" % i for i in range(len(roles)))
|
ph = ','.join("'role%d'" % i for i in range(len(roles)))
|
||||||
for i, r in enumerate(roles):
|
for i, r in enumerate(roles):
|
||||||
params['role%d' % i] = r
|
params['role%d' % i] = r
|
||||||
cond = ("status='human_pending' AND current_role IN (%s)" % ph)
|
cond = ("status='human_pending' AND assignee_role IN (%s)" % ph)
|
||||||
elif queue == 'mine':
|
elif queue == 'mine':
|
||||||
cond = ("status IN ('human_processing','staff_replied') "
|
cond = ("status IN ('human_processing','staff_replied') "
|
||||||
"AND current_assignee=${u}$")
|
"AND assignee_id=${u}$")
|
||||||
params["u"] = str(user_id or '')
|
params["u"] = str(user_id or '')
|
||||||
else:
|
else:
|
||||||
if not _is_admin(roles) and not any(r.startswith('owner.') for r in roles):
|
if not _is_admin(roles) and not any(r.startswith('owner.') for r in roles):
|
||||||
@ -559,11 +559,11 @@ async def list_staff_tickets(user_id, roles=None, queue='all', limit=100):
|
|||||||
cond = "status NOT IN ('closed','cancelled') OR closed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"
|
cond = "status NOT IN ('closed','cancelled') OR closed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"
|
||||||
recs = await sor.sqlExe(
|
recs = await sor.sqlExe(
|
||||||
"SELECT t.id, t.ticket_no, t.title, t.category, t.priority, t.status, "
|
"SELECT t.id, t.ticket_no, t.title, t.category, t.priority, t.status, "
|
||||||
"t.current_role, t.current_assignee, t.customer_org_id, t.created_at, "
|
"t.assignee_role, t.assignee_id, t.customer_org_id, t.created_at, "
|
||||||
"t.updated_at, o.orgname AS customer_org_name, u.username AS assignee_name "
|
"t.updated_at, o.orgname AS customer_org_name, u.username AS assignee_name "
|
||||||
"FROM tk_tickets t "
|
"FROM tk_tickets t "
|
||||||
"LEFT JOIN organization o ON t.customer_org_id=o.id "
|
"LEFT JOIN organization o ON t.customer_org_id=o.id "
|
||||||
"LEFT JOIN users u ON t.current_assignee=u.id "
|
"LEFT JOIN users u ON t.assignee_id=u.id "
|
||||||
"WHERE " + cond + " ORDER BY t.updated_at DESC LIMIT ${lim}$", params)
|
"WHERE " + cond + " ORDER BY t.updated_at DESC LIMIT ${lim}$", params)
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
return [_rec_to_dict(r) for r in (recs or [])]
|
return [_rec_to_dict(r) for r in (recs or [])]
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
待办由工单状态派生(不建通知表,I7 同一事项只发一次):
|
待办由工单状态派生(不建通知表,I7 同一事项只发一次):
|
||||||
- 客户:status ∈ {agent_replied, staff_replied} 且工单是我的 → 「工单回复」待确认
|
- 客户:status ∈ {agent_replied, staff_replied} 且工单是我的 → 「工单回复」待确认
|
||||||
- 运维角色池:status=human_pending 且 current_role ∈ 我的角色 → 「工单认领」
|
- 运维角色池:status=human_pending 且 assignee_role ∈ 我的角色 → 「工单认领」
|
||||||
- 受理人:status=human_processing 且 current_assignee=我 → 「工单处理」
|
- 受理人:status=human_processing 且 assignee_id=我 → 「工单处理」
|
||||||
|
|
||||||
provider 签名:fn(user_id, roles, limit) -> [todo dict]
|
provider 签名:fn(user_id, roles, limit) -> [todo dict]
|
||||||
todo dict 字段与 list_my_human_todos 现有两来源对齐 + source='ticket' + detail_url。
|
todo dict 字段与 list_my_human_todos 现有两来源对齐 + source='ticket' + detail_url。
|
||||||
@ -37,15 +37,15 @@ async def list_ticket_todos(user_id, roles=None, limit=100):
|
|||||||
# ① 客户视角:我的工单已有回复待确认
|
# ① 客户视角:我的工单已有回复待确认
|
||||||
conds.append("(customer_user_id=${u}$ AND status IN ('agent_replied','staff_replied'))")
|
conds.append("(customer_user_id=${u}$ AND status IN ('agent_replied','staff_replied'))")
|
||||||
# ② 受理人视角:我认领的工单待处理
|
# ② 受理人视角:我认领的工单待处理
|
||||||
conds.append("(current_assignee=${u}$ AND status='human_processing')")
|
conds.append("(assignee_id=${u}$ AND status='human_processing')")
|
||||||
# ③ 角色池视角:待认领且受理角色∈我的角色
|
# ③ 角色池视角:待认领且受理角色∈我的角色
|
||||||
if roles:
|
if roles:
|
||||||
ph = ','.join("'r%d'" % i for i in range(len(roles)))
|
ph = ','.join("'r%d'" % i for i in range(len(roles)))
|
||||||
for i, r in enumerate(roles):
|
for i, r in enumerate(roles):
|
||||||
params['r%d' % i] = r
|
params['r%d' % i] = r
|
||||||
conds.append("(status='human_pending' AND current_role IN (%s))" % ph)
|
conds.append("(status='human_pending' AND assignee_role IN (%s))" % ph)
|
||||||
sql = ("SELECT id, ticket_no, title, status, current_role, customer_user_id, "
|
sql = ("SELECT id, ticket_no, title, status, assignee_role, customer_user_id, "
|
||||||
"current_assignee, updated_at, created_at FROM tk_tickets "
|
"assignee_id, updated_at, created_at FROM tk_tickets "
|
||||||
"WHERE (" + ' OR '.join(conds) + ") "
|
"WHERE (" + ' OR '.join(conds) + ") "
|
||||||
"ORDER BY updated_at DESC LIMIT ${lim}$")
|
"ORDER BY updated_at DESC LIMIT ${lim}$")
|
||||||
recs = await sor.sqlExe(sql, params)
|
recs = await sor.sqlExe(sql, params)
|
||||||
@ -60,13 +60,13 @@ async def list_ticket_todos(user_id, roles=None, limit=100):
|
|||||||
badge = '工单回复'
|
badge = '工单回复'
|
||||||
summary = '%s | 请确认是否解决' % STATUS_LABEL.get(status, status)
|
summary = '%s | 请确认是否解决' % STATUS_LABEL.get(status, status)
|
||||||
ttype = 'ticket_customer_confirm'
|
ttype = 'ticket_customer_confirm'
|
||||||
elif str(d.get('current_assignee', '')) == uid and status == 'human_processing':
|
elif str(d.get('assignee_id', '')) == uid and status == 'human_processing':
|
||||||
badge = '工单处理'
|
badge = '工单处理'
|
||||||
summary = '您受理的工单待处理'
|
summary = '您受理的工单待处理'
|
||||||
ttype = 'ticket_staff_process'
|
ttype = 'ticket_staff_process'
|
||||||
elif status == 'human_pending':
|
elif status == 'human_pending':
|
||||||
badge = '工单认领'
|
badge = '工单认领'
|
||||||
summary = '受理角色:%s | 待认领' % str(d.get('current_role', ''))
|
summary = '受理角色:%s | 待认领' % str(d.get('assignee_role', ''))
|
||||||
ttype = 'ticket_staff_claim'
|
ttype = 'ticket_staff_claim'
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -44,8 +44,8 @@ md_parts.append('**状态**:' + STATUS_LABEL.get(status, status)
|
|||||||
+ ' | **优先级**:' + str(t.get('priority', '')))
|
+ ' | **优先级**:' + str(t.get('priority', '')))
|
||||||
if view == 'staff':
|
if view == 'staff':
|
||||||
md_parts.append('**客户机构**:' + str(t.get('customer_org_id', ''))
|
md_parts.append('**客户机构**:' + str(t.get('customer_org_id', ''))
|
||||||
+ ' | **受理角色**:' + (str(t.get('current_role', '')) or '-')
|
+ ' | **受理角色**:' + (str(t.get('assignee_role', '')) or '-')
|
||||||
+ ' | **受理人**:' + (str(t.get('current_assignee', '')) or '-'))
|
+ ' | **受理人**:' + (str(t.get('assignee_id', '')) or '-'))
|
||||||
md_parts.append('')
|
md_parts.append('')
|
||||||
md_parts.append('---')
|
md_parts.append('---')
|
||||||
md_parts.append('### 往来记录')
|
md_parts.append('### 往来记录')
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
"row_options": {
|
"row_options": {
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"exclouded": ["id", "customer_org_id", "current_assignee"],
|
"exclouded": ["id", "customer_org_id", "assignee_id"],
|
||||||
"alters": {
|
"alters": {
|
||||||
"category": {"uitype": "code", "data": [
|
"category": {"uitype": "code", "data": [
|
||||||
{"value": "consult", "text": "使用咨询"}, {"value": "fault", "text": "故障报修"},
|
{"value": "consult", "text": "使用咨询"}, {"value": "fault", "text": "故障报修"},
|
||||||
@ -60,7 +60,7 @@
|
|||||||
{"name": "priority", "title": "优先级", "type": "str", "length": 16, "cwidth": 6},
|
{"name": "priority", "title": "优先级", "type": "str", "length": 16, "cwidth": 6},
|
||||||
{"name": "status", "title": "状态", "type": "str", "length": 32, "cwidth": 10},
|
{"name": "status", "title": "状态", "type": "str", "length": 32, "cwidth": 10},
|
||||||
{"name": "customer_org_name", "title": "客户机构", "type": "str", "length": 100, "cwidth": 14},
|
{"name": "customer_org_name", "title": "客户机构", "type": "str", "length": 100, "cwidth": 14},
|
||||||
{"name": "current_role", "title": "受理角色", "type": "str", "length": 64, "cwidth": 12},
|
{"name": "assignee_role", "title": "受理角色", "type": "str", "length": 64, "cwidth": 12},
|
||||||
{"name": "assignee_name", "title": "受理人", "type": "str", "length": 32, "cwidth": 8},
|
{"name": "assignee_name", "title": "受理人", "type": "str", "length": 32, "cwidth": 8},
|
||||||
{"name": "updated_at", "title": "最后更新", "type": "timestamp", "cwidth": 14}
|
{"name": "updated_at", "title": "最后更新", "type": "timestamp", "cwidth": 14}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user