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": "customer_org_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": "current_assignee", "title": "当前受理人", "type": "str", "length": 32},
|
||||
{"name": "assignee_role", "title": "当前受理角色", "type": "str", "length": 64},
|
||||
{"name": "assignee_id", "title": "当前受理人", "type": "str", "length": 32},
|
||||
{"name": "processing_owner", "title": "agent处理锁标记", "type": "str", "length": 64},
|
||||
{"name": "followup_count", "title": "客户追问次数", "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": "idx_tk_cust", "idxtype": "index", "idxfields": ["customer_org_id", "status"]},
|
||||
{"name": "idx_tk_status", "idxtype": "index", "idxfields": ["status"]},
|
||||
{"name": "idx_tk_role_status", "idxtype": "index", "idxfields": ["current_role", "status"]},
|
||||
{"name": "idx_tk_assignee", "idxtype": "index", "idxfields": ["current_assignee", "status"]}
|
||||
{"name": "idx_tk_role_status", "idxtype": "index", "idxfields": ["assignee_role", "status"]},
|
||||
{"name": "idx_tk_assignee", "idxtype": "index", "idxfields": ["assignee_id", "status"]}
|
||||
],
|
||||
"codes": [
|
||||
{"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)
|
||||
只能授 logined 而不能授固定角色——D4 决策:转派目标是 owner 组织类型下的
|
||||
**任意动态角色**(role 表运行时可增删),RBAC 路径权限无法枚举动态角色,
|
||||
业务权限由服务端 _check_staff_access 校验(操作者须持有工单 current_role,
|
||||
业务权限由服务端 _check_staff_access 校验(操作者须持有工单 assignee_role,
|
||||
admin/superuser 豁免)。客户侧数据由 I5 服务端自过滤(customer_user_id/org_id)。
|
||||
- owner.maintainer / owner.admin / owner.superuser:管理页壳 /ticket/manage/**。
|
||||
菜单「工单管理」入口也按角色 Jinja 分支渲染,客户直接访问 URL 403。
|
||||
@ -42,7 +42,7 @@ PATHS_LOGINED = [
|
||||
f"/{MOD}/api/ticket_followup.dspy",
|
||||
f"/{MOD}/api/ticket_confirm.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/ticket_claim.dspy",
|
||||
f"/{MOD}/api/ticket_staff_reply.dspy",
|
||||
|
||||
@ -226,8 +226,8 @@ async def process_one_ticket(ticket_id):
|
||||
# 基础设施故障(T4):fail_rounds+1 回退 new;超限转人工
|
||||
if fail_rounds + 1 >= fail_max:
|
||||
moved = await core._cas_status(sor, ticket_id, core.S_AGENT_PROCESSING, {
|
||||
'status': core.S_HUMAN_PENDING, 'current_role': default_role,
|
||||
'current_assignee': None, 'processing_owner': None,
|
||||
'status': core.S_HUMAN_PENDING, 'assignee_role': default_role,
|
||||
'assignee_id': None, 'processing_owner': None,
|
||||
'agent_fail_rounds': fail_rounds + 1})
|
||||
if moved:
|
||||
await core._add_message(
|
||||
@ -263,8 +263,8 @@ async def process_one_ticket(ticket_id):
|
||||
|
||||
# T3:答不了 → 转人工
|
||||
moved = await core._cas_status(sor, ticket_id, core.S_AGENT_PROCESSING, {
|
||||
'status': core.S_HUMAN_PENDING, 'current_role': default_role,
|
||||
'current_assignee': None, 'processing_owner': None, 'agent_fail_rounds': 0})
|
||||
'status': core.S_HUMAN_PENDING, 'assignee_role': default_role,
|
||||
'assignee_id': None, 'processing_owner': None, 'agent_fail_rounds': 0})
|
||||
if moved:
|
||||
await core._add_message(
|
||||
sor, ticket_id, 'agent', core.AGENT_USER, '',
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
"""ticket/core.py — 工单状态机与全部迁移动作。
|
||||
|
||||
不变量(设计文档 §4.3):
|
||||
- I1 处理方唯一:status+current_role+current_assignee 联合表达;迁移全部乐观锁
|
||||
- I1 处理方唯一:status+assignee_role+assignee_id 联合表达;迁移全部乐观锁
|
||||
(UPDATE ... WHERE status=前置态 + SELECT 验证,照 pipeline_service claim 范式)
|
||||
- I2 human_pending ⟺ current_role 非空 AND current_assignee NULL
|
||||
- I3 human_processing/staff_replied ⟹ current_assignee 非空
|
||||
- I2 human_pending ⟺ assignee_role 非空 AND assignee_id NULL
|
||||
- I3 human_processing/staff_replied ⟹ assignee_id 非空
|
||||
- I4 受理方每次变更必写 tk_transfers(同事务)
|
||||
- I5 客户只见自己工单 + visibility='customer' 消息
|
||||
- I6 终态(closed/cancelled)不接受任何迁移
|
||||
@ -215,8 +215,8 @@ async def create_ticket(user_id, org_id, title, description, category='other',
|
||||
'status': S_NEW,
|
||||
'customer_org_id': str(org_id or ''),
|
||||
'customer_user_id': str(user_id),
|
||||
'current_role': None,
|
||||
'current_assignee': None,
|
||||
'assignee_role': None,
|
||||
'assignee_id': None,
|
||||
'processing_owner': None,
|
||||
'followup_count': 0,
|
||||
'agent_fail_rounds': 0,
|
||||
@ -281,8 +281,8 @@ async def ticket_followup(ticket_id, user_id, org_id, content):
|
||||
if cur > followup_max:
|
||||
default_role = await _get_param(sor, 'ticket_default_role', 'owner.maintainer')
|
||||
ok = await _cas_status(sor, ticket_id, S_AGENT_REPLIED, {
|
||||
'status': S_HUMAN_PENDING, 'current_role': default_role,
|
||||
'current_assignee': None, 'followup_count': cur})
|
||||
'status': S_HUMAN_PENDING, 'assignee_role': default_role,
|
||||
'assignee_id': None, 'followup_count': cur})
|
||||
if ok:
|
||||
await _add_message(sor, ticket_id, 'agent', AGENT_USER, '',
|
||||
'客户追问超过 %d 次,自动转人工处理。' % followup_max,
|
||||
@ -317,7 +317,7 @@ async def ticket_confirm(ticket_id, user_id, org_id):
|
||||
return False, ('TK_E002', '工单当前状态为 %s,不能确认关闭(需已有回复)' % status)
|
||||
ok = await _cas_status(sor, ticket_id, status, {
|
||||
'status': S_CLOSED, 'closed_at': _now(), 'close_reason': 'resolved',
|
||||
'current_role': None, 'current_assignee': None})
|
||||
'assignee_role': None, 'assignee_id': None})
|
||||
if not ok:
|
||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||
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', '智能助手正在处理中,请稍后再取消')
|
||||
ok = await _cas_status(sor, ticket_id, status, {
|
||||
'status': S_CANCELLED, 'closed_at': _now(), 'close_reason': 'cancelled',
|
||||
'current_role': None, 'current_assignee': None})
|
||||
'assignee_role': None, 'assignee_id': None})
|
||||
if not ok:
|
||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||
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):
|
||||
"""运维侧访问:admin/superuser 豁免;否则须持有工单 current_role。"""
|
||||
"""运维侧访问:admin/superuser 豁免;否则须持有工单 assignee_role。"""
|
||||
if _is_admin(roles):
|
||||
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
|
||||
|
||||
|
||||
@ -368,9 +368,9 @@ async def ticket_claim(ticket_id, user_id):
|
||||
return False, E_NOT_FOUND
|
||||
if not await _check_staff_access(sor, t, user_id, roles):
|
||||
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, {
|
||||
'status': S_HUMAN_PROCESSING, 'current_assignee': user_id})
|
||||
'status': S_HUMAN_PROCESSING, 'assignee_id': user_id})
|
||||
if not ok:
|
||||
return False, ('TK_E005', '该工单刚被他人认领,请刷新队列')
|
||||
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)
|
||||
if not t:
|
||||
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)):
|
||||
return False, ('TK_E001', '只有当前受理人可以回复该工单')
|
||||
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', ''))
|
||||
if status != S_HUMAN_PROCESSING:
|
||||
return False, ('TK_E002', '工单当前状态为 %s,不能转派(需认领后 human_processing)' % status)
|
||||
cur_role = str(getattr(t, 'current_role', '') or '')
|
||||
cur_assignee = str(getattr(t, 'current_assignee', '') or '')
|
||||
cur_role = str(getattr(t, 'assignee_role', '') or '')
|
||||
cur_assignee = str(getattr(t, 'assignee_id', '') or '')
|
||||
|
||||
if to_user:
|
||||
# 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.')]
|
||||
new_role = owner_roles[0] if owner_roles else cur_role
|
||||
ok = await _cas_status(sor, ticket_id, S_HUMAN_PROCESSING, {
|
||||
'status': S_HUMAN_PROCESSING, 'current_role': new_role,
|
||||
'current_assignee': to_user})
|
||||
'status': S_HUMAN_PROCESSING, 'assignee_role': new_role,
|
||||
'assignee_id': to_user})
|
||||
if not ok:
|
||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||
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 组织类型角色列表中,请重新选择')
|
||||
full_role = to_role if '.' in to_role else 'owner.' + to_role
|
||||
ok = await _cas_status(sor, ticket_id, S_HUMAN_PROCESSING, {
|
||||
'status': S_HUMAN_PENDING, 'current_role': full_role,
|
||||
'current_assignee': None})
|
||||
'status': S_HUMAN_PENDING, 'assignee_role': full_role,
|
||||
'assignee_id': None})
|
||||
if not ok:
|
||||
return False, ('TK_E002', '工单状态刚发生变化,请刷新后重试')
|
||||
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
|
||||
wsql = (' WHERE ' + ' AND '.join(where)) if where else ''
|
||||
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 "
|
||||
"FROM tk_tickets" + wsql + " ORDER BY created_at DESC LIMIT ${lim}$", params)
|
||||
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)))
|
||||
for i, r in enumerate(roles):
|
||||
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':
|
||||
cond = ("status IN ('human_processing','staff_replied') "
|
||||
"AND current_assignee=${u}$")
|
||||
"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):
|
||||
@ -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)"
|
||||
recs = await sor.sqlExe(
|
||||
"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 "
|
||||
"FROM tk_tickets t "
|
||||
"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)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return [_rec_to_dict(r) for r in (recs or [])]
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
|
||||
待办由工单状态派生(不建通知表,I7 同一事项只发一次):
|
||||
- 客户:status ∈ {agent_replied, staff_replied} 且工单是我的 → 「工单回复」待确认
|
||||
- 运维角色池:status=human_pending 且 current_role ∈ 我的角色 → 「工单认领」
|
||||
- 受理人:status=human_processing 且 current_assignee=我 → 「工单处理」
|
||||
- 运维角色池:status=human_pending 且 assignee_role ∈ 我的角色 → 「工单认领」
|
||||
- 受理人:status=human_processing 且 assignee_id=我 → 「工单处理」
|
||||
|
||||
provider 签名:fn(user_id, roles, limit) -> [todo dict]
|
||||
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("(current_assignee=${u}$ AND status='human_processing')")
|
||||
conds.append("(assignee_id=${u}$ AND status='human_processing')")
|
||||
# ③ 角色池视角:待认领且受理角色∈我的角色
|
||||
if roles:
|
||||
ph = ','.join("'r%d'" % i for i in range(len(roles)))
|
||||
for i, r in enumerate(roles):
|
||||
params['r%d' % i] = r
|
||||
conds.append("(status='human_pending' AND current_role IN (%s))" % ph)
|
||||
sql = ("SELECT id, ticket_no, title, status, current_role, customer_user_id, "
|
||||
"current_assignee, updated_at, created_at FROM tk_tickets "
|
||||
conds.append("(status='human_pending' AND assignee_role IN (%s))" % ph)
|
||||
sql = ("SELECT id, ticket_no, title, status, assignee_role, customer_user_id, "
|
||||
"assignee_id, updated_at, created_at FROM tk_tickets "
|
||||
"WHERE (" + ' OR '.join(conds) + ") "
|
||||
"ORDER BY updated_at DESC LIMIT ${lim}$")
|
||||
recs = await sor.sqlExe(sql, params)
|
||||
@ -60,13 +60,13 @@ async def list_ticket_todos(user_id, roles=None, limit=100):
|
||||
badge = '工单回复'
|
||||
summary = '%s | 请确认是否解决' % STATUS_LABEL.get(status, status)
|
||||
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 = '工单处理'
|
||||
summary = '您受理的工单待处理'
|
||||
ttype = 'ticket_staff_process'
|
||||
elif status == 'human_pending':
|
||||
badge = '工单认领'
|
||||
summary = '受理角色:%s | 待认领' % str(d.get('current_role', ''))
|
||||
summary = '受理角色:%s | 待认领' % str(d.get('assignee_role', ''))
|
||||
ttype = 'ticket_staff_claim'
|
||||
else:
|
||||
continue
|
||||
|
||||
@ -44,8 +44,8 @@ md_parts.append('**状态**:' + STATUS_LABEL.get(status, status)
|
||||
+ ' | **优先级**:' + str(t.get('priority', '')))
|
||||
if view == 'staff':
|
||||
md_parts.append('**客户机构**:' + str(t.get('customer_org_id', ''))
|
||||
+ ' | **受理角色**:' + (str(t.get('current_role', '')) or '-')
|
||||
+ ' | **受理人**:' + (str(t.get('current_assignee', '')) or '-'))
|
||||
+ ' | **受理角色**:' + (str(t.get('assignee_role', '')) or '-')
|
||||
+ ' | **受理人**:' + (str(t.get('assignee_id', '')) or '-'))
|
||||
md_parts.append('')
|
||||
md_parts.append('---')
|
||||
md_parts.append('### 往来记录')
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
},
|
||||
"row_options": {
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "customer_org_id", "current_assignee"],
|
||||
"exclouded": ["id", "customer_org_id", "assignee_id"],
|
||||
"alters": {
|
||||
"category": {"uitype": "code", "data": [
|
||||
{"value": "consult", "text": "使用咨询"}, {"value": "fault", "text": "故障报修"},
|
||||
@ -60,7 +60,7 @@
|
||||
{"name": "priority", "title": "优先级", "type": "str", "length": 16, "cwidth": 6},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 32, "cwidth": 10},
|
||||
{"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": "updated_at", "title": "最后更新", "type": "timestamp", "cwidth": 14}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user