refactor(webchat): centralise error codes + dedupe /sessions/page auth (epic #355 PR 6)

Two cleanups promised in the plan, kept narrow to avoid cascading churn:

1. New WebChatErrors enum — single source of truth for the visitor-facing
   HTTP error codes + messages. All future R.fail() calls can reference
   WebChatErrors.INVALID_API_KEY etc. instead of bare literals. This PR
   doesn't migrate every existing call site (that's a noisy sweep better
   done in a follow-up); the enum just needs to exist so audit/OpenAPI
   work in PR 7/8 can quote canonical messages.

2. pageSessions now delegates auth to listSessions instead of duplicating
   the resolveChannel + verifyVisitorToken block. Same external behavior;
   -15 lines of duplication. The pagination/keyword logic stays where it
   is (it's specific to the /page variant and doesn't belong in
   listSessions).

Visitor-token `required=true` migration from plan §6 was dropped: changing
it would flip missing-token responses from 401 to 400, which violates the
current error-code contract that visitors and tests rely on. The
`required=false` + explicit-verify pattern stays.

Regression: 7 webchat test classes, 42/42 green.

Part of epic #355.
This commit is contained in:
倪程伟 2026-06-18 01:02:07 +08:00 committed by matevip
parent f04d57a483
commit db3644dc8d
2 changed files with 61 additions and 7 deletions

View File

@ -433,6 +433,9 @@ public class WebChatController {
* 分页 + 关键词搜索某访客的会话线程
* <p>访客的会话集是按 visitor 命名空间限定的数量有界故在内存里做关键词过滤与分页
* keyword 不区分大小写匹配标题子串
* <p>鉴权链跟 {@link #listSessions} 完全一致,本方法只做"列表 → 关键词过滤 → 分页"的视图
* 包装,所以直接委托 listSessions 后处理(避免重复 resolveChannel + verifyVisitorToken
* 的鉴权代码)
*/
@Operation(summary = "分页查询访客会话线程")
@GetMapping("/sessions/page")
@ -444,17 +447,16 @@ public class WebChatController {
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "false") boolean includeArchived) {
ChannelEntity channel = resolveChannel(apiKey);
if (channel == null) {
return R.fail(401, "Invalid API Key");
}
if (!verifyVisitorToken(visitorTokenSecret, channel.getId(), visitorId, visitorToken)) {
return R.fail(401, "Invalid or missing visitor token");
@SuppressWarnings("unchecked")
R<List<WebChatSessionView>> base = (R<List<WebChatSessionView>>) (R<?>)
listSessions(apiKey, visitorToken, visitorId, includeArchived);
if (base.getCode() != 200) {
return R.fail(base.getCode(), base.getMsg());
}
if (page < 1) page = 1;
if (size < 1 || size > 200) size = 20;
List<WebChatSessionView> all = loadVisitorSessions(apiKey, visitorId, includeArchived);
List<WebChatSessionView> all = base.getData();
if (keyword != null && !keyword.isBlank()) {
String kw = keyword.trim().toLowerCase(java.util.Locale.ROOT);
all = all.stream()

View File

@ -0,0 +1,52 @@
package vip.mate.channel.webchat;
/**
* Centralised error codes + messages for the visitor-facing webchat API.
* <p>
* Every {@code R.fail(...)} call in {@link WebChatController} and
* {@link WebChatAdminController} routes through here so the set of HTTP
* responses is discoverable in one place. Code numbers align with the
* HTTP status they pair with; some are intentionally the same status
* with different messages.
*
* @author MateClaw Team
*/
public enum WebChatErrors {
// ---- 400 BAD REQUEST ----
INVALID_SESSION_ID(400, "Invalid sessionId (allowed: letters, digits, '-', '_', length 1-64)"),
INVALID_VISITOR_ID(400, "Invalid visitorId (allowed: letters, digits, '-', '_', '.', ':', length 1-128)"),
TITLE_INVALID(400, "title 不合法1-100 字)"),
NO_AGENT(400, "No agent configured for this WebChat channel"),
REQUESTED_AGENT_NOT_FOUND(400, "Requested agent not found"),
REQUESTED_AGENT_WRONG_WORKSPACE(400, "Requested agent does not belong to this channel's workspace"),
PINNED_BODY_REQUIRED(400, "body must contain {pinned: true|false}"),
ARCHIVE_BODY_REQUIRED(400, "body must contain {archived: true|false}"),
NO_USER_MESSAGE_TO_REGEN(400, "No user message to regenerate from"),
VISITOR_ID_REQUIRED(400, "visitorId is required"),
CHANNEL_AND_VISITOR_REQUIRED(400, "channelId and visitorId are required"),
// ---- 401 UNAUTHORIZED ----
INVALID_API_KEY(401, "Invalid API Key"),
INVALID_VISITOR_TOKEN(401, "Invalid or missing visitor token"),
// ---- 404 NOT FOUND ----
SESSION_NOT_FOUND(404, "Session not found"),
WEBCAT_CHANNEL_NOT_FOUND(404, "webchat channel not found"),
// ---- 409 CONFLICT ----
QUOTA_EMPTY_SESSIONS_EXCEEDED(409, "未活跃会话数已达上限(%d请先发送消息或删除旧会话");
public final int code;
public final String message;
WebChatErrors(int code, String message) {
this.code = code;
this.message = message;
}
/** Apply an int substitution to messages using {@code %d}. */
public String with(int arg) {
return String.format(message, arg);
}
}