From 5398ac155e494f3604e487a48f69085c42d0fbdc Mon Sep 17 00:00:00 2001 From: fuuzen <80118151@qq.com> Date: Mon, 3 Mar 2025 22:44:00 +0800 Subject: [PATCH 1/2] feat: add icalendar server api --- backend/go.mod | 5 ++ backend/go.sum | 5 ++ backend/internal/handler/handler.go | 5 ++ backend/internal/handler/ical.go | 109 ++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 backend/internal/handler/ical.go diff --git a/backend/go.mod b/backend/go.mod index c0678a0..420050f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -17,6 +17,11 @@ require ( golang.org/x/crypto v0.32.0 ) +require ( + github.com/lestrrat-go/bufferpool v0.0.0-20180220091733-e7784e1b3e37 // indirect + github.com/pkg/errors v0.9.1 // indirect +) + require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/backend/go.sum b/backend/go.sum index 6934385..91310ba 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -36,8 +36,12 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lestrrat-go/bufferpool v0.0.0-20180220091733-e7784e1b3e37 h1:px5km9KhQGUKiPWIVZ++FErEMTd06XEuMi2OswGMrqI= +github.com/lestrrat-go/bufferpool v0.0.0-20180220091733-e7784e1b3e37/go.mod h1:vs3QXw2t0jsgjLEG7JZt0uE1jcSkxnQr+5bhQ80UJHE= github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ= github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -47,6 +51,7 @@ github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go index 9447abd..e89dabd 100644 --- a/backend/internal/handler/handler.go +++ b/backend/internal/handler/handler.go @@ -120,4 +120,9 @@ func (h *Handler) RegisterRoutes() { }) }) }) + + // iCalendar 订阅服务, iCalendar 协议只能是公开的, 所以不需要身份验证 + h.Mux.Route("/", func(r chi.Router) { + r.Get("/{id}.ics", h.icalHandler) + }) } diff --git a/backend/internal/handler/ical.go b/backend/internal/handler/ical.go new file mode 100644 index 0000000..8421dd1 --- /dev/null +++ b/backend/internal/handler/ical.go @@ -0,0 +1,109 @@ +package handler + +import ( + "net/http" + "slices" + "strconv" + "time" + + "github.com/go-chi/chi/v5" + "github.com/sysu-ecnc-dev/shift-manager/backend/internal/domain" +) + +func (h *Handler) icalHandler(w http.ResponseWriter, r *http.Request) { + userID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + if err != nil { + h.internalServerError(w, r, err) + } + + plans, err := h.repository.GetAllSchedulePlans() + if err != nil { + h.internalServerError(w, r, err) + } + + var icsContent string = `BEGIN:VCALENDAR +CALSCALE:GREGORIAN +PRODID:SYSU-ECNC +VERSION:2.0 +X-APPLE-CALENDAR-COLOR:#CC73E1 +X-WR-CALNAME:ECNC值班日程 +BEGIN:VTIMEZONE +TZID:Asia/Shanghai +BEGIN:STANDARD +DTSTART:19890917T020000 +RRULE:FREQ=YEARLY;UNTIL=19910914T170000Z;BYMONTH=9;BYDAY=3SU +TZNAME:GMT+8 +TZOFFSETFROM:+0900 +TZOFFSETTO:+0800 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19910414T020000 +RDATE:19910414T020000 +TZNAME:GMT+8 +TZOFFSETFROM:+0800 +TZOFFSETTO:+0900 +END:DAYLIGHT +END:VTIMEZONE` + + for _, plan := range plans { + res, err := h.repository.GetSchedulingResultBySchedulePlanID(plan.ID) + if err != nil { + h.internalServerError(w, r, err) + } + + template, err := h.repository.GetScheduleTemplate(plan.ScheduleTemplateID) + if err != nil { + h.internalServerError(w, r, err) + } + + shifts := template.Shifts + shiftMap := make(map[int64]*domain.ScheduleTemplateShift) + for _, shift := range shifts { + shiftMap[shift.ID] = &shift + } + + for _, resultShift := range res.Shifts { + for _, item := range resultShift.Items { + if (item.PrincipalID != nil && *item.PrincipalID == userID) || slices.Contains(item.AssistantIDs, userID) { + icsContent += "\nBEGIN:VEVENT" + if *item.PrincipalID == userID { + icsContent += "\nDESCRIPTION:负责人岗值班" + } else { + icsContent += "\nDESCRIPTION:普通助理岗值班" + } + templateShift := shiftMap[resultShift.ShiftID] + firstTime := plan.ActiveStartTime + for { + if firstTime.Weekday() == time.Weekday(item.Day-1) { + break + } else { + firstTime = firstTime.Add(24 * time.Hour) + } + } + startTime, err := time.Parse("15:04:05", templateShift.StartTime) + if err != nil { + h.internalServerError(w, r, err) + } + endTime, err := time.Parse("15:04:05", templateShift.EndTime) + if err != nil { + h.internalServerError(w, r, err) + } + icsContent += "\nDTEND;TZID=Asia/Shanghai:" + firstTime.Add(time.Duration(endTime.Hour())*time.Hour).Format("20060102T150405") + icsContent += "\nDTSTAMP:" + plan.CreatedAt.Format("20060102T150405") + "Z" + icsContent += "\nDTSTART;TZID=Asia/Shanghai:" + firstTime.Add(time.Duration(startTime.Hour())*time.Hour).Format("20060102T150405") + icsContent += "\nLAST-MODIFIED:" + time.Now().Format("20060102T150405") + "Z" + icsContent += "\nLOCATION:中山大学东校园" + icsContent += "\nRRULE:FREQ=WEEKLY;UNTIL=" + plan.ActiveEndTime.Format("20060102T150405") + "Z" + icsContent += "\nSEQUENCE:0" + icsContent += "\nSUMMARY:ECNC值班日程" + icsContent += "\nTRANSP:OPAQUE" + icsContent += "\nUID:SYSU-ECNC-" + strconv.FormatInt(userID, 16) + "-" + strconv.FormatInt(resultShift.ShiftID, 16) + "-" + strconv.FormatInt(int64(item.Day), 16) + icsContent += "\nEND:VEVENT" + } + } + } + } + w.Header().Set("Content-Type", "text/calendar") + w.Header().Set("Content-Disposition", "inline; filename=calendar.ics") + w.Write(([]byte)(icsContent + "\nEND:VCALENDAR")) +} From b9d363a10d63c5b4aef5e0c8209be51b70059f1d Mon Sep 17 00:00:00 2001 From: fuuzen <80118151@qq.com> Date: Tue, 4 Mar 2025 00:10:32 +0800 Subject: [PATCH 2/2] feat: add temp link for ical --- frontend/src/routes/_dashboard/index.tsx | 34 ++++++++++++++++++++++-- frontend/vite.config.ts | 7 ++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/_dashboard/index.tsx b/frontend/src/routes/_dashboard/index.tsx index c95be8d..23523e2 100644 --- a/frontend/src/routes/_dashboard/index.tsx +++ b/frontend/src/routes/_dashboard/index.tsx @@ -1,16 +1,46 @@ +import { getMyInfo } from "@/lib/api"; +import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; - +import { toast } from "sonner"; export const Route = createFileRoute("/_dashboard/")({ component: RouteComponent, }); function RouteComponent() { + const { + data: myInfo, + isPending, + isError, + error, + } = useQuery({ + queryKey: ["my-info"], + queryFn: () => getMyInfo().then((res) => res.data.data), + }); + + if (isPending) return null; + + if (isError) { + toast.error(error.message); + return null; + } + + const icsURL = "https://" + import.meta.env.DOMAIN + "/api/" + myInfo.id + ".ics" + return ( -