Repository navigation
fix: chat user group add member - #7816
Conversation
| > | ||
| <el-option v-for="user in userOptions" :key="user.id" :label="user.nick_name" :value="user.id" /> | ||
| </el-select> | ||
| </el-form-item> |
There was a problem hiding this comment.
Here is a review of the provided code changes, focusing on logic, potential bugs, and optimization.
1. Race Condition in loadUserOptions
Issue: The asynchronous call to ChatUserApi.getChatUser does not handle stale responses. If loadUserOptions is called multiple times quickly (e.g., via remote search), a later request finishing before an earlier one will result in the earlier (stale) response overwriting the newer data.
Suggestion: Implement a cleanup function or use an AbortController to check if the current request is still active before updating state.
2. Logic Flaw: Missing Selected Options on Refresh
Issue: When loadUserOptions fetches new users based on a keyword, it merges them with existing selected options using [...new Map([...selectedUserOptions, ...users]...)]. However, if the initial load (open()) hasn't finished or if memberForm.userIds contains IDs not present in userOptions.value initially, those options might be lost or fail to render correctly until they are fetched again. More critically, if a user is selected but then filtered out by a search (server-side), the UI might rely entirely on this local merge logic. Ensure that El-Select behaves correctly when the option is not in the current options list but is in the v-model. Element Plus usually requires the option to exist in the slot or dynamically generated list to show the label properly.
Suggestion: Consider caching full user objects for all selected IDs locally so their labels can always be rendered regardless of the current userOptions list content from the API.
3. Potential Memory/Performance Issue with Large Lists
Issue: [...new Map(...).values()] creates a new array every time options are loaded. For very large datasets, this is inefficient.
Suggestion: This is generally acceptable for typical chat user lists, but ensure Map keys are stable. The current approach is O(N) which is fine unless N is very large (>10k items).
4. Missing Initial Load State Handling
Issue: open() sets visible.value = true and then calls loadUserOptions(). It's good that loading is handled, but ensure the dialog shows an appropriate empty/loading state if userOptions is initially empty before the API responds.
Suggestion: Verify that the UI gracefully handles the initial empty userOptions.value = [] state.
5. Form Reset Logic
Issue: Not visible in the diff, but resetData is called on @closed. Ensure it resets userOptions back to a default list (or clears it) and memberForm.userIds to an empty array to prevent leakage between dialog open/close cycles.
✅ Suggested Fix for Race Condition & Label Rendering
// Use a counter to ignore stale responses
let requestCounter = 0;
const selectedUsersCache = reactive(new Map<string, ChatUserBase>()); // Cache selected users' details locally
function loadUserOptions(keyword = '') {
const currentRequest = ++requestCounter;
optionsLoading.value = true
return ChatUserApi.getChatUser(keyword ? { nick_name: keyword } : undefined)
.then((users) => {
// Ignore response if a newer request has started
if (currentRequest !== requestCounter) return;
users.forEach(user => selectedUsersCache.set(user.id, user));
// Combine cached selected users + newly fetched users for display
// You may need to filter 'users' to avoid duplicates if you don't want to re-cache everything,
// but here we just update cache.
// To render options properly, El-Select needs the option elements.
// If you are only rendering 'userOptions', ensure it includes ALL currently selected users
// whose details are known.
const selectedSet = new Set(memberForm.userIds);
const currentOptions = new Map<string, ChatUserBase>();
// Add currently selected users from cache
selectedSet.forEach(id => {
const cachedUser = selectedUsersCache.get(id);
if (cachedUser) currentOptions.set(id, cachedUser);
});
// Add fetched users
users.forEach(user => currentOptions.set(user.id, user));
userOptions.value = Array.from(currentOptions.values());
})
.catch(() => {
// Handle error
})
.finally(() => {
if (currentRequest === requestCounter) {
optionsLoading.value = false;
}
});
}Summary of Recommendations:
- Add race condition protection (using a counter or signal).
- Verify label rendering for selected items when they are not in the current
filterableresult set. Caching selected user objects separately is a robust pattern. - Ensure
resetDataclears bothmemberForm.userIdsand any local caches likeuserOptionsorselectedUsersCache.
| users = query_set[:200] | ||
| return ChatUserInstanceSerializer(users, many=True).data | ||
|
|
||
|
|
There was a problem hiding this comment.
The change is functionally reasonable, but there are several issues and improvements to consider:
Potential issues
-
Silent truncation to 200 records
query_set[:200]silently limits results.- This changes existing
list()behavior and can break callers expecting all users. - Prefer explicit pagination (
limit/offsetor page/page_size) and return metadata if needed.
-
Case-sensitive / inconsistent nickname search
nick_name__containsis case-sensitive on PostgreSQL, but may behave differently on MySQL/SQLite depending on collation.- For user-facing search, use
nick_name__icontainsfor consistent case-insensitive matching.
-
No input normalization or validation
if nick_name:skips empty string but not whitespace-only values.- A very long
nick_namecan trigger an expensive search. - Suggested:
if nick_name: nick_name = nick_name.strip() if nick_name: query_set = query_set.filter(nick_name__icontains=nick_name)
- Add a max length check if this is exposed through an API.
-
Performance risk with
containsnick_name__containsusually generatesLIKE '%value%', which cannot use a normal B-tree index efficiently.- For large tables, consider:
istartswithif prefix search is acceptable,- trigram/GIN index for PostgreSQL,
- full-text search for more advanced search.
- Also ensure
create_timeis indexed because oforder_by("-create_time").
-
Non-deterministic ordering when
create_timevalues tie- Add a tie-breaker for stable ordering/pagination:
order_by("-create_time", "-id")
- Add a tie-breaker for stable ordering/pagination:
-
Possible N+1 queries
- If
ChatUserInstanceSerializerincludes related fields, add appropriateselect_related()/prefetch_related().
- If
Minor style points
query_setis usually namedqueryset.- Consider type hints and explicit pagination parameters:
@classmethod def list(cls, nick_name: str | None = None, limit: int = 200, offset: int = 0):
- Validate/clamp
limitto prevent abuse.
Suggested revised version
@classmethod
def list(cls, nick_name=None, limit=200, offset=0):
queryset = ChatUser.objects.all()
if nick_name:
nick_name = nick_name.strip()
if nick_name:
queryset = queryset.filter(nick_name__icontains=nick_name)
queryset = queryset.order_by("-create_time", "-id")
users = queryset[offset:offset + limit]
return ChatUserInstanceSerializer(users, many=True).dataOverall: the change works, but the hard-coded 200 limit and case-sensitive search are the main concerns. Add pagination, use icontains, normalize input, and verify indexing/performance for nickname searches.
| return get<ChatUserBase[]>(`${prefix}/list`, query) | ||
| } | ||
|
|
||
| /** 获取对话用户分页列表。 */ |
There was a problem hiding this comment.
No syntax irregularity in this small diff. The change is backward-compatible for normal callers, but there are a few points to verify:
-
Confirm what the second argument of
getmeans- If
get<T>(url, params)expects query parameters directly, the change is correct. - If
get<T>(url, config)is axios-like or expects a request config, passingquerydirectly is a bug. You would need:get<ChatUserBase[]>(`${prefix}/list`, { params: query })
- If
-
Dict<unknown>is too loose- It allows non-serializable values such as nested objects, arrays,
Date, or functions, whose serialization may be inconsistent. - Prefer an explicit query DTO:
interface ChatUserListQuery { keyword?: string page?: number pageSize?: number } const getChatUser = (query?: ChatUserListQuery) => { return get<ChatUserBase[]>(`${prefix}/list`, query) }
- If the query is user-controlled, consider allowlisting supported parameters.
- It allows non-serializable values such as nested objects, arrays,
-
Optional argument can be accidentally passed an event object
- If
getChatUseris used directly as an event handler, e.g.onClick={getChatUser}, the click event may be passed asqueryand forwarded to the API. - Use a wrapper in such cases:
onClick={() => getChatUser()}
- If
-
Ensure
undefined/empty query values are handled- Many HTTP clients skip
undefinedquery values, but verify yourgetwrapper does too. - If
queryis{}, it may produce the same request, but if your client appends?, normalize it.
- Many HTTP clients skip
-
Caching implications
- If results are cached by URL or a query key, make sure the new query parameters are included in the cache key.
- If different query values return different lists, existing cache invalidation logic may need updating.
-
Server compatibility
- Verify
/system/chat_user/listaccepts the new query parameters. Unknown parameters may be ignored by some servers but can cause400errors on others.
- Verify
Minor naming suggestion: getChatUser returns a list, so getChatUserList would be clearer, but this is not a bug.
Recommended implementation, assuming get accepts query params directly:
interface ChatUserListQuery {
keyword?: string
page?: number
pageSize?: number
}
const getChatUser = (query?: ChatUserListQuery) => {
return get<ChatUserBase[]>(`${prefix}/list`, query)
}
fix: chat user group add member