fix: keep workbench user ids compatible

This commit is contained in:
richjohnrambo
2026-09-16 18:02:19 +08:00
parent 5789b85663
commit 903a77f2c5
4 changed files with 163 additions and 30 deletions
+11 -11
View File
@@ -238,7 +238,7 @@ func (a *API) attendanceRecords(w http.ResponseWriter, r *http.Request, p *auth.
Status: r.URL.Query().Get("status"),
Cursor: r.URL.Query().Get("cursor"),
Limit: limit(r, 50, 100),
UserID: parseInt64(r.URL.Query().Get("user_id")),
UserID: parseUserIDQuery(r.URL.Query().Get("user_id")),
})
if err != nil {
writeStoreError(w, r, err)
@@ -402,11 +402,11 @@ func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal
}
var body struct {
Title string `json:"title"`
RecorderUserID int64 `json:"recorder_user_id"`
RecorderUserID userID `json:"recorder_user_id"`
StartAt string `json:"start_at"`
EndAt string `json:"end_at"`
Location string `json:"location"`
AttendeeUserIDs []int64 `json:"attendee_user_ids"`
AttendeeUserIDs []userID `json:"attendee_user_ids"`
ReminderMinutes []int `json:"reminder_minutes"`
}
if !decodeBody(w, r, &body) {
@@ -424,9 +424,9 @@ func (a *API) meetings(w http.ResponseWriter, r *http.Request, p *auth.Principal
}
reminders, _ := json.Marshal(body.ReminderMinutes)
meeting, err := a.store.CreateMeeting(r.Context(), p.UserID, store.Meeting{
MeetingID: "mtg_" + randomToken(12), RecorderID: body.RecorderUserID, Title: strings.TrimSpace(body.Title), StartAt: startAt, EndAt: endAt,
MeetingID: "mtg_" + randomToken(12), RecorderID: int64(body.RecorderUserID), Title: strings.TrimSpace(body.Title), StartAt: startAt, EndAt: endAt,
Location: strings.TrimSpace(body.Location), ReminderMinutes: string(reminders), IdempotencyKey: idempotencyKey,
}, body.AttendeeUserIDs)
}, parseUserIDList(body.AttendeeUserIDs))
if err != nil {
writeStoreError(w, r, err)
return
@@ -460,13 +460,13 @@ func (a *API) meetingResource(w http.ResponseWriter, r *http.Request, p *auth.Pr
case tail == "" && r.Method == http.MethodPatch:
var body struct {
Title *string `json:"title"`
RecorderUserID *int64 `json:"recorder_user_id"`
RecorderUserID optionalUserID `json:"recorder_user_id"`
Version int `json:"version"`
}
if !decodeBody(w, r, &body) {
return
}
meeting, err := a.store.UpdateMeeting(r.Context(), p.UserID, meetingID, store.MeetingPatch{Title: body.Title, RecorderID: body.RecorderUserID, Version: body.Version})
meeting, err := a.store.UpdateMeeting(r.Context(), p.UserID, meetingID, store.MeetingPatch{Title: body.Title, RecorderID: body.RecorderUserID.ptr(), Version: body.Version})
if err != nil {
writeStoreError(w, r, err)
return
@@ -730,13 +730,13 @@ func (a *API) dailyReportReminders(w http.ResponseWriter, r *http.Request, p *au
}
var body struct {
Date string `json:"date"`
UserIDs []int64 `json:"user_ids"`
UserIDs []userID `json:"user_ids"`
}
if !decodeBody(w, r, &body) {
return
}
if !a.publishWorkbenchEvent(w, r, p, "daily_report", "daily_report.reminded", strings.TrimSpace(body.Date),
body.UserIDs, "日报提醒", strings.TrimSpace(body.Date),
parseUserIDList(body.UserIDs), "日报提醒", strings.TrimSpace(body.Date),
map[string]any{"date": strings.TrimSpace(body.Date), "idempotency_key": idempotencyKey, "reminded_count": len(body.UserIDs)}) {
return
}
@@ -1000,8 +1000,8 @@ func limit(r *http.Request, fallback, max int) int {
return parsed
}
func parseInt64(value string) int64 {
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
func parseUserIDQuery(value string) int64 {
parsed, _ := parseUserID(value)
return parsed
}
+11 -11
View File
@@ -54,7 +54,7 @@ func organizationMemberPayloads(members []store.OrganizationMember) []map[string
func organizationMemberPayload(member store.OrganizationMember) map[string]any {
payload := map[string]any{
"user_id": member.UserID,
"user_id": formatUserID(member.UserID),
"department_id": member.DepartmentID,
"display_name": member.DisplayName,
"position": member.Position,
@@ -133,7 +133,7 @@ func attendanceRecordPayload(record *store.AttendanceRecord) map[string]any {
}
return map[string]any{
"record_id": record.RecordID,
"user_id": record.UserID,
"user_id": formatUserID(record.UserID),
"date": record.Date.Format("2006-01-02"),
"rule_id": record.RuleID,
"shift_id": record.ShiftID,
@@ -195,7 +195,7 @@ func attendanceAppealPayload(appeal *store.AttendanceAppeal) map[string]any {
data := map[string]any{
"appeal_id": appeal.AppealID,
"record_id": appeal.RecordID,
"user_id": appeal.UserID,
"user_id": formatUserID(appeal.UserID),
"appeal_type": appeal.AppealType,
"reason": appeal.Reason,
"target_action": appeal.TargetAction,
@@ -229,9 +229,9 @@ func meetingPayload(meeting store.Meeting, viewer int64) map[string]any {
data := map[string]any{
"meeting_id": meeting.MeetingID,
"title": meeting.Title,
"organizer_id": meeting.OrganizerID,
"organizer_id": formatUserID(meeting.OrganizerID),
"organizer": userSummary(meeting.OrganizerID),
"recorder_user_id": recorderID,
"recorder_user_id": formatUserID(recorderID),
"recorder": userSummary(recorderID),
"start_at": meeting.StartAt.UTC().Format(time.RFC3339Nano),
"end_at": meeting.EndAt.UTC().Format(time.RFC3339Nano),
@@ -263,7 +263,7 @@ func meetingPayload(meeting store.Meeting, viewer int64) map[string]any {
func meetingParticipantPayloads(participants []store.MeetingParticipant) []map[string]any {
items := make([]map[string]any, 0, len(participants))
for _, participant := range participants {
item := map[string]any{"user_id": participant.UserID, "response": participant.Response}
item := map[string]any{"user_id": formatUserID(participant.UserID), "response": participant.Response}
if participant.ResponseAt != nil {
item["response_at"] = participant.ResponseAt.UTC().Format(time.RFC3339Nano)
}
@@ -318,7 +318,7 @@ func meetingNoteFilePayload(file store.MeetingNoteFile, viewer int64) map[string
"preview_status": file.PreviewStatus,
"preview_mode": file.PreviewMode,
"preview_file_id": file.PreviewFileID,
"uploader_user_id": file.UploaderID,
"uploader_user_id": formatUserID(file.UploaderID),
"uploader": userSummary(file.UploaderID),
"version": file.Version,
"permissions": map[string]any{
@@ -343,7 +343,7 @@ func dailyReportPayloads(reports []store.DailyReport) []map[string]any {
func dailyReportPayload(report store.DailyReport) map[string]any {
data := map[string]any{
"report_id": report.ReportID,
"author_id": report.AuthorID,
"author_id": formatUserID(report.AuthorID),
"report_date": report.ReportDate.Format("2006-01-02"),
"content": report.Content,
"content_short": preview(report.Content),
@@ -372,7 +372,7 @@ func monthlyReportPayloads(reports []store.MonthlyReport) []map[string]any {
func monthlyReportPayload(report store.MonthlyReport) map[string]any {
data := map[string]any{
"report_id": report.ReportID,
"author_id": report.AuthorID,
"author_id": formatUserID(report.AuthorID),
"month": report.Month,
"work_summary": report.WorkSummary,
"next_plan": report.NextPlan,
@@ -408,7 +408,7 @@ func mailboxConfigPayload(config store.MailboxConfig) map[string]any {
func mailboxSubmissionPayload(submission store.MailboxSubmission) map[string]any {
return map[string]any{
"submission_id": submission.SubmissionID,
"sender_id": submission.SenderID,
"sender_id": formatUserID(submission.SenderID),
"subject": submission.Subject,
"anonymous": submission.Anonymous,
"attachment_file_ids": jsonList(submission.AttachmentFileIDs),
@@ -439,7 +439,7 @@ func meetingNotePermissions(meeting store.Meeting, viewer int64) map[string]any
}
func userSummary(userID int64) map[string]any {
return map[string]any{"user_id": userID, "display_name": userID}
return map[string]any{"user_id": formatUserID(userID), "display_name": formatUserID(userID)}
}
func jsonList(raw string) []any {
+100
View File
@@ -0,0 +1,100 @@
package httpapi
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"strconv"
"strings"
)
type userID int64
func (id *userID) UnmarshalJSON(raw []byte) error {
value, err := parseUserIDJSON(raw)
if err != nil {
return err
}
*id = userID(value)
return nil
}
type optionalUserID struct {
value int64
set bool
}
func (id *optionalUserID) UnmarshalJSON(raw []byte) error {
if strings.TrimSpace(string(raw)) == "null" {
id.set = true
id.value = 0
return nil
}
value, err := parseUserIDJSON(raw)
if err != nil {
return err
}
id.set = true
id.value = value
return nil
}
func (id optionalUserID) ptr() *int64 {
if !id.set {
return nil
}
return &id.value
}
func parseUserIDJSON(raw []byte) (int64, error) {
var text string
if err := json.Unmarshal(raw, &text); err == nil {
return parseUserID(text)
}
var number int64
if err := json.Unmarshal(raw, &number); err == nil {
return number, nil
}
return 0, fmt.Errorf("invalid user id")
}
func parseUserID(value string) (int64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, fmt.Errorf("empty user id")
}
if strings.HasPrefix(value, "usr") {
decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(strings.TrimPrefix(value, "usr"))
if err != nil || len(decoded) < 8 {
return 0, fmt.Errorf("invalid user id")
}
return int64(binary.LittleEndian.Uint64(decoded[:8])), nil
}
return strconv.ParseInt(value, 10, 64)
}
func formatUserID(id int64) string {
if id == 0 {
return ""
}
src := make([]byte, 8)
dst := make([]byte, base64.URLEncoding.WithPadding(base64.NoPadding).EncodedLen(8))
binary.LittleEndian.PutUint64(src, uint64(id))
base64.URLEncoding.WithPadding(base64.NoPadding).Encode(dst, src)
return "usr" + string(dst)
}
func parseUserIDList(values []userID) []int64 {
items := make([]int64, 0, len(values))
seen := make(map[int64]bool, len(values))
for _, value := range values {
id := int64(value)
if id == 0 || seen[id] {
continue
}
seen[id] = true
items = append(items, id)
}
return items
}
+33
View File
@@ -0,0 +1,33 @@
package httpapi
import (
"encoding/json"
"testing"
)
func TestUserIDJSONAcceptsIMUserID(t *testing.T) {
var body struct {
RecorderUserID userID `json:"recorder_user_id"`
AttendeeUserIDs []userID `json:"attendee_user_ids"`
}
raw := []byte(`{"recorder_user_id":"usrag7w9xJ6hto","attendee_user_ids":["usr1RqTFgHSenM"]}`)
if err := json.Unmarshal(raw, &body); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if body.RecorderUserID == 0 || len(body.AttendeeUserIDs) != 1 || body.AttendeeUserIDs[0] == 0 {
t.Fatalf("decoded zero ids: %+v", body)
}
if got := formatUserID(int64(body.RecorderUserID)); got != "usrag7w9xJ6hto" {
t.Fatalf("roundtrip recorder=%q", got)
}
}
func TestUserIDJSONStillAcceptsNumericID(t *testing.T) {
var id userID
if err := json.Unmarshal([]byte(`1573862062872858624`), &id); err != nil {
t.Fatalf("unmarshal numeric: %v", err)
}
if id != 1573862062872858624 {
t.Fatalf("id=%d", id)
}
}