Files
olons_server/internal/store/mysql.go
T

827 lines
33 KiB
Go

package store
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"git.118im.com/im_team/im_workbench/internal/config"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
type Store struct {
db *sqlx.DB
}
func Open(cfg config.MySQLConfig) (*Store, error) {
db, err := sqlx.Open("mysql", cfg.DSN)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(30)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return nil, err
}
return &Store{db: db}, nil
}
func (s *Store) Close() error {
if s == nil || s.db == nil {
return nil
}
return s.db.Close()
}
func (s *Store) WorkbenchModules(ctx context.Context, userID int64) ([]WorkbenchModule, int, error) {
var modules []WorkbenchModule
err := s.db.SelectContext(ctx, &modules, `SELECT code,name,IFNULL(icon_file_id,'') AS icon_file_id,route,sort_order,0 AS unread_count,enabled,permission
FROM im_workbench_apps WHERE enabled=1 ORDER BY sort_order ASC,id ASC`)
if err != nil {
return nil, 0, err
}
for i := range modules {
modules[i].UnreadCount = 0
if modules[i].Permission == "" {
modules[i].Permission = "enabled"
}
}
return modules, 1, nil
}
func (s *Store) MeetingUnreadCount(ctx context.Context, userID int64) (int, error) {
var count int
err := s.db.GetContext(ctx, &count, `SELECT COUNT(*) FROM im_system_notifications
WHERE user_id=? AND read_at IS NULL AND event_type LIKE 'meeting.%'`, userID)
if isMissingTable(err) {
return 0, nil
}
return count, err
}
func (s *Store) Departments(ctx context.Context, userID int64, parentID, cursor string, limit int) ([]OrganizationDepartment, string, error) {
if parentID == "" {
parentID = "root"
}
var items []OrganizationDepartment
query := `SELECT d.department_id,d.name,d.parent_id,
(SELECT COUNT(*) FROM im_org_departments c WHERE c.parent_id=d.department_id) AS child_count,
(SELECT COUNT(*) FROM im_org_members m WHERE m.department_id=d.department_id AND m.employment_state='active') AS member_count,
d.version
FROM im_org_departments d WHERE d.parent_id=?`
args := []any{parentID}
if cursor != "" {
query += ` AND d.department_id>?`
args = append(args, cursor)
}
query += ` ORDER BY d.department_id ASC LIMIT ?`
args = append(args, limit+1)
if err := s.db.SelectContext(ctx, &items, query, args...); err != nil {
return nil, "", err
}
return trimDepartmentPage(items, limit)
}
func (s *Store) DepartmentMembers(ctx context.Context, userID int64, departmentID, cursor string, limit int) ([]OrganizationMember, string, error) {
var items []OrganizationMember
query := `SELECT user_id,department_id,display_name,position,IFNULL(mobile,'') AS mobile,IFNULL(email,'') AS email,
IFNULL(avatar_file_id,'') AS avatar_file_id,employment_state AS status,contactable,version
FROM im_org_members WHERE department_id=? AND employment_state='active'`
args := []any{departmentID}
if cursor != "" {
query += ` AND user_id>?`
args = append(args, cursor)
}
query += ` ORDER BY user_id ASC LIMIT ?`
args = append(args, limit+1)
if err := s.db.SelectContext(ctx, &items, query, args...); err != nil {
return nil, "", err
}
return trimMemberPage(items, limit)
}
func (s *Store) Members(ctx context.Context, userID int64, departmentID, keyword, cursor string, limit int) ([]OrganizationMember, string, error) {
var items []OrganizationMember
where := []string{"employment_state='active'"}
args := []any{}
if departmentID != "" {
where = append(where, "department_id=?")
args = append(args, departmentID)
}
if keyword != "" {
where = append(where, "(display_name LIKE ? OR position LIKE ? OR mobile LIKE ?)")
like := "%" + keyword + "%"
args = append(args, like, like, like)
}
if cursor != "" {
where = append(where, "user_id>?")
args = append(args, cursor)
}
query := `SELECT user_id,department_id,display_name,position,IFNULL(mobile,'') AS mobile,IFNULL(email,'') AS email,
IFNULL(avatar_file_id,'') AS avatar_file_id,employment_state AS status,contactable,version
FROM im_org_members WHERE ` + strings.Join(where, " AND ") + ` ORDER BY user_id ASC LIMIT ?`
args = append(args, limit+1)
if err := s.db.SelectContext(ctx, &items, query, args...); err != nil {
return nil, "", err
}
return trimMemberPage(items, limit)
}
func (s *Store) AttendanceConfig(ctx context.Context, userID int64, date time.Time) (*AttendanceConfig, error) {
var rule AttendanceRule
if err := s.db.GetContext(ctx, &rule, `SELECT rule_id,name,workday,location_required,wifi_required,allow_outside_checkin,appeal_allowed,NULL AS appeal_deadline
FROM im_attendance_rules WHERE state='active' ORDER BY id ASC LIMIT 1`); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return &AttendanceConfig{Date: date, Timezone: "Asia/Shanghai", Rule: AttendanceRule{}, Shifts: []AttendanceShift{}}, nil
}
return nil, err
}
var shifts []AttendanceShift
if err := s.db.SelectContext(ctx, &shifts, `SELECT shift_id,rule_id,name,work_start,work_end,checkin_start,checkout_end
FROM im_attendance_shifts WHERE rule_id=? AND state='active' ORDER BY sort_order ASC,id ASC`, rule.RuleID); err != nil {
return nil, err
}
record, _ := s.attendanceRecordByDate(ctx, userID, date)
return &AttendanceConfig{Date: date, Timezone: "Asia/Shanghai", Rule: rule, Shifts: shifts, TodayRecord: record}, nil
}
func (s *Store) AttendanceRecords(ctx context.Context, userID int64, opts AttendanceListOptions) ([]AttendanceRecord, string, *AttendanceStats, error) {
where := []string{"1=1"}
args := []any{}
if opts.UserID > 0 {
where = append(where, "user_id=?")
args = append(args, opts.UserID)
} else if opts.Scope != "managed" {
where = append(where, "user_id=?")
args = append(args, userID)
}
if opts.Date != "" {
where = append(where, "record_date=?")
args = append(args, opts.Date)
}
if opts.From != "" {
where = append(where, "record_date>=?")
args = append(args, opts.From)
}
if opts.To != "" {
where = append(where, "record_date<=?")
args = append(args, opts.To)
}
if opts.Status != "" {
where = append(where, "status=?")
args = append(args, opts.Status)
}
if opts.Cursor != "" {
where = append(where, "id<?")
args = append(args, opts.Cursor)
}
query := `SELECT record_id,user_id,record_date,rule_id,shift_id,status,exceptions,appeal_status,version,created_at,updated_at
FROM im_attendance_records WHERE ` + strings.Join(where, " AND ") + ` ORDER BY id DESC LIMIT ?`
args = append(args, opts.Limit+1)
var records []AttendanceRecord
if err := s.db.SelectContext(ctx, &records, query, args...); err != nil {
return nil, "", nil, err
}
next := ""
if len(records) > opts.Limit {
next = records[opts.Limit-1].RecordID
records = records[:opts.Limit]
}
for i := range records {
records[i].Checkins, _ = s.attendanceCheckins(ctx, records[i].RecordID)
}
stats := &AttendanceStats{ExpectedCount: len(records), CheckedCount: len(records)}
return records, next, stats, nil
}
type AttendanceListOptions struct {
Scope string
From string
To string
Date string
Status string
Cursor string
Limit int
UserID int64
}
func (s *Store) CreateAttendanceCheckin(ctx context.Context, userID int64, checkinID string, req AttendanceCheckinRequest) (*AttendanceRecord, *AttendanceCheckin, error) {
now := time.Now().UTC().Round(time.Millisecond)
recordID := fmt.Sprintf("att_%d_%s_%s", userID, req.Date.Format("20060102"), req.ShiftID)
result := "normal"
status := "checked_in"
if req.Action == "check_out" {
status = "normal"
}
tx, err := s.db.BeginTxx(ctx, nil)
if err != nil {
return nil, nil, err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, `INSERT INTO im_attendance_records(record_id,user_id,record_date,rule_id,shift_id,status,exceptions,appeal_status,version,created_at,updated_at)
VALUES(?,?,?,?,?,?,'[]','none',1,?,?)
ON DUPLICATE KEY UPDATE status=VALUES(status),version=version+1,updated_at=VALUES(updated_at)`,
recordID, userID, req.Date, req.RuleID, req.ShiftID, status, now, now)
if err != nil {
return nil, nil, err
}
_, err = tx.ExecContext(ctx, `INSERT INTO im_attendance_checkins(checkin_id,record_id,user_id,record_date,action,rule_id,shift_id,client_time,server_time,result,location,wifi,device,idempotency_key)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
checkinID, recordID, userID, req.Date, req.Action, req.RuleID, req.ShiftID, req.ClientTime, now, result, req.Location, req.Wifi, req.Device, req.IdempotencyKey)
if isDuplicate(err) {
_ = tx.Rollback()
return s.attendanceRecordAndCheckinByIdempotency(ctx, userID, req.IdempotencyKey)
}
if err != nil {
return nil, nil, err
}
if err := tx.Commit(); err != nil {
return nil, nil, err
}
return s.attendanceRecordAndCheckinByIdempotency(ctx, userID, req.IdempotencyKey)
}
type AttendanceCheckinRequest struct {
Date time.Time
Action string
RuleID string
ShiftID string
ClientTime time.Time
Location string
Wifi string
Device string
IdempotencyKey string
}
func (s *Store) AttendanceAppeals(ctx context.Context, userID int64, status, cursor string, limit int) ([]AttendanceAppeal, string, error) {
where := []string{"user_id=?"}
args := []any{userID}
if status != "" {
where = append(where, "status=?")
args = append(args, status)
}
if cursor != "" {
where = append(where, "id<?")
args = append(args, cursor)
}
query := `SELECT appeal_id,record_id,user_id,appeal_type,reason,target_action,target_time,attachment_file_ids,status,version,submitted_at,created_at,updated_at
FROM im_attendance_appeals WHERE ` + strings.Join(where, " AND ") + ` ORDER BY id DESC LIMIT ?`
args = append(args, limit+1)
var items []AttendanceAppeal
if err := s.db.SelectContext(ctx, &items, query, args...); err != nil {
return nil, "", err
}
next := ""
if len(items) > limit {
next = items[limit-1].AppealID
items = items[:limit]
}
return items, next, nil
}
func (s *Store) CreateAttendanceAppeal(ctx context.Context, userID int64, appeal AttendanceAppeal, idempotencyKey string) (*AttendanceAppeal, error) {
now := time.Now().UTC().Round(time.Millisecond)
if appeal.Status == "" {
appeal.Status = "pending"
}
_, err := s.db.ExecContext(ctx, `INSERT INTO im_attendance_appeals(appeal_id,record_id,user_id,appeal_type,reason,target_action,target_time,attachment_file_ids,status,version,idempotency_key,submitted_at,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,1,?,?,?,?)`,
appeal.AppealID, appeal.RecordID, userID, appeal.AppealType, appeal.Reason, appeal.TargetAction, appeal.TargetTime, appeal.AttachmentFileIDs, appeal.Status, idempotencyKey, now, now, now)
if isDuplicate(err) {
var existing AttendanceAppeal
if getErr := s.db.GetContext(ctx, &existing, `SELECT appeal_id,record_id,user_id,appeal_type,reason,target_action,target_time,attachment_file_ids,status,version,submitted_at,created_at,updated_at
FROM im_attendance_appeals WHERE user_id=? AND idempotency_key=? LIMIT 1`, userID, idempotencyKey); getErr != nil {
return nil, getErr
}
return &existing, nil
}
if err != nil {
return nil, err
}
return &appeal, nil
}
func (s *Store) Meetings(ctx context.Context, userID int64, cursor string, limit int) ([]Meeting, string, error) {
var items []Meeting
query := `SELECT meeting_id,organizer_id,recorder_user_id,title,start_at,end_at,location,state,reminder_minutes,idempotency_key,version,canceled_at,created_at,updated_at
FROM im_meetings WHERE organizer_id=? OR EXISTS(SELECT 1 FROM im_meeting_participants p WHERE p.meeting_id=im_meetings.meeting_id AND p.user_id=?)`
args := []any{userID, userID}
if cursor != "" {
query += ` AND meeting_id>?`
args = append(args, cursor)
}
query += ` ORDER BY start_at DESC,id DESC LIMIT ?`
args = append(args, limit+1)
if err := s.db.SelectContext(ctx, &items, query, args...); err != nil {
return nil, "", err
}
next := ""
if len(items) > limit {
next = items[limit-1].MeetingID
items = items[:limit]
}
for i := range items {
items[i].Participants, _ = s.meetingParticipants(ctx, items[i].MeetingID)
}
return items, next, nil
}
func (s *Store) Meeting(ctx context.Context, userID int64, meetingID string) (*Meeting, error) {
var meeting Meeting
if err := s.db.GetContext(ctx, &meeting, `SELECT meeting_id,organizer_id,recorder_user_id,title,start_at,end_at,location,state,reminder_minutes,idempotency_key,version,canceled_at,created_at,updated_at
FROM im_meetings WHERE meeting_id=? LIMIT 1`, meetingID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
meeting.Participants, _ = s.meetingParticipants(ctx, meeting.MeetingID)
if !meetingVisible(meeting, userID) {
return nil, ErrForbidden
}
return &meeting, nil
}
func (s *Store) CreateMeeting(ctx context.Context, userID int64, meeting Meeting, attendees []int64) (*Meeting, error) {
now := time.Now().UTC().Round(time.Millisecond)
if meeting.RecorderID == 0 {
meeting.RecorderID = userID
}
if meeting.State == "" {
meeting.State = "scheduled"
}
reminders := meeting.ReminderMinutes
if reminders == "" {
reminders = "[]"
}
tx, err := s.db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, `INSERT INTO im_meetings(meeting_id,organizer_id,recorder_user_id,title,start_at,end_at,location,state,reminder_minutes,idempotency_key,version,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,1,?,?)`, meeting.MeetingID, userID, meeting.RecorderID, meeting.Title, meeting.StartAt, meeting.EndAt, meeting.Location, meeting.State, reminders, meeting.IdempotencyKey, now, now)
if isDuplicate(err) && meeting.IdempotencyKey != "" {
_ = tx.Rollback()
var existingID string
if getErr := s.db.GetContext(ctx, &existingID, `SELECT meeting_id FROM im_meetings WHERE organizer_id=? AND idempotency_key=? LIMIT 1`, userID, meeting.IdempotencyKey); getErr != nil {
return nil, getErr
}
return s.Meeting(ctx, userID, existingID)
}
if err != nil {
return nil, err
}
seen := map[int64]bool{userID: true}
attendees = append(attendees, userID, meeting.RecorderID)
for _, uid := range attendees {
if uid <= 0 || seen[uid] {
continue
}
seen[uid] = true
_, err = tx.ExecContext(ctx, `INSERT INTO im_meeting_participants(meeting_id,user_id,response) VALUES(?,?,?)`, meeting.MeetingID, uid, "pending")
if err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
return s.Meeting(ctx, userID, meeting.MeetingID)
}
func (s *Store) UpdateMeeting(ctx context.Context, userID int64, meetingID string, patch MeetingPatch) (*Meeting, error) {
meeting, err := s.Meeting(ctx, userID, meetingID)
if err != nil {
return nil, err
}
if meeting.OrganizerID != userID || meeting.State != "scheduled" || patch.Version != meeting.Version {
return nil, ErrConflict
}
sets := []string{"version=version+1", "updated_at=?"}
args := []any{time.Now().UTC().Round(time.Millisecond)}
if patch.Title != nil {
sets = append(sets, "title=?")
args = append(args, strings.TrimSpace(*patch.Title))
}
if patch.RecorderID != nil {
sets = append(sets, "recorder_user_id=?")
args = append(args, *patch.RecorderID)
}
args = append(args, meetingID)
if _, err := s.db.ExecContext(ctx, `UPDATE im_meetings SET `+strings.Join(sets, ",")+` WHERE meeting_id=?`, args...); err != nil {
return nil, err
}
return s.Meeting(ctx, userID, meetingID)
}
type MeetingPatch struct {
Title *string
RecorderID *int64
Version int
}
func (s *Store) CancelMeeting(ctx context.Context, userID int64, meetingID string) (*Meeting, error) {
now := time.Now().UTC().Round(time.Millisecond)
if _, err := s.db.ExecContext(ctx, `UPDATE im_meetings SET state='canceled',canceled_at=?,version=version+1,updated_at=? WHERE meeting_id=? AND organizer_id=? AND state='scheduled'`, now, now, meetingID, userID); err != nil {
return nil, err
}
return s.Meeting(ctx, userID, meetingID)
}
func (s *Store) RespondMeeting(ctx context.Context, userID int64, meetingID, response string, version int) (*Meeting, error) {
now := time.Now().UTC().Round(time.Millisecond)
if _, err := s.db.ExecContext(ctx, `UPDATE im_meeting_participants SET response=?,response_at=? WHERE meeting_id=? AND user_id=?`, response, now, meetingID, userID); err != nil {
return nil, err
}
return s.Meeting(ctx, userID, meetingID)
}
func (s *Store) MeetingNotes(ctx context.Context, userID int64, queryText, cursor string, limit int) ([]Meeting, string, error) {
items, next, err := s.Meetings(ctx, userID, cursor, limit)
if err != nil {
return nil, "", err
}
filtered := items[:0]
now := time.Now()
for _, item := range items {
if item.EndAt.Before(now) && item.State != "canceled" && (queryText == "" || strings.Contains(item.Title, queryText)) {
filtered = append(filtered, item)
}
}
return filtered, next, nil
}
func (s *Store) MeetingNoteFiles(ctx context.Context, userID int64, meetingID string) ([]MeetingNoteFile, error) {
if _, err := s.Meeting(ctx, userID, meetingID); err != nil {
return nil, err
}
var files []MeetingNoteFile
if err := s.db.SelectContext(ctx, &files, `SELECT note_file_id,meeting_id,file_id,uploader_id,file_name,mime_type,file_size,file_etag,status,preview_status,preview_mode,preview_file_id,version,created_at,updated_at
FROM im_meeting_note_files WHERE meeting_id=? AND status='active' ORDER BY id DESC`, meetingID); err != nil {
return nil, err
}
return files, nil
}
func (s *Store) CreateMeetingNoteFile(ctx context.Context, userID int64, meetingID, noteFileID, fileID, idempotencyKey string) (*MeetingNoteFile, error) {
meeting, err := s.Meeting(ctx, userID, meetingID)
if err != nil {
return nil, err
}
if meeting.RecorderID != userID && meeting.OrganizerID != userID {
return nil, ErrForbidden
}
now := time.Now().UTC().Round(time.Millisecond)
fileName, mimeType, fileSize, fileETag := fileID, "", int64(0), ""
_ = s.db.QueryRowxContext(ctx, `SELECT IFNULL(mimetype,''),IFNULL(size,0),IFNULL(etag,'') FROM im_fileuploads WHERE id=? LIMIT 1`, fileID).Scan(&mimeType, &fileSize, &fileETag)
_, err = s.db.ExecContext(ctx, `INSERT INTO im_meeting_note_files(note_file_id,meeting_id,file_id,uploader_id,file_name,mime_type,file_size,file_etag,status,preview_status,preview_mode,preview_file_id,version,idempotency_key,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,'active','processing','native','',1,?,?,?)`, noteFileID, meetingID, fileID, userID, fileName, mimeType, fileSize, fileETag, idempotencyKey, now, now)
if isDuplicate(err) && idempotencyKey != "" {
var existing MeetingNoteFile
if getErr := s.db.GetContext(ctx, &existing, `SELECT note_file_id,meeting_id,file_id,uploader_id,file_name,mime_type,file_size,file_etag,status,preview_status,preview_mode,preview_file_id,version,created_at,updated_at
FROM im_meeting_note_files WHERE meeting_id=? AND uploader_id=? AND idempotency_key=? LIMIT 1`, meetingID, userID, idempotencyKey); getErr != nil {
return nil, getErr
}
return &existing, nil
}
if err != nil {
return nil, err
}
var created MeetingNoteFile
if err := s.db.GetContext(ctx, &created, `SELECT note_file_id,meeting_id,file_id,uploader_id,file_name,mime_type,file_size,file_etag,status,preview_status,preview_mode,preview_file_id,version,created_at,updated_at
FROM im_meeting_note_files WHERE note_file_id=? LIMIT 1`, noteFileID); err != nil {
return nil, err
}
return &created, nil
}
func (s *Store) ReplaceMeetingNoteFile(ctx context.Context, userID int64, meetingID, noteFileID, fileID, idempotencyKey string, version int) (*MeetingNoteFile, error) {
meeting, err := s.Meeting(ctx, userID, meetingID)
if err != nil {
return nil, err
}
if meeting.RecorderID != userID && meeting.OrganizerID != userID {
return nil, ErrForbidden
}
fileName, mimeType, fileSize, fileETag := fileID, "", int64(0), ""
_ = s.db.QueryRowxContext(ctx, `SELECT IFNULL(mimetype,''),IFNULL(size,0),IFNULL(etag,'') FROM im_fileuploads WHERE id=? LIMIT 1`, fileID).Scan(&mimeType, &fileSize, &fileETag)
res, err := s.db.ExecContext(ctx, `UPDATE im_meeting_note_files SET file_id=?,file_name=?,mime_type=?,file_size=?,file_etag=?,preview_status='processing',preview_file_id='',version=version+1,idempotency_key=?,updated_at=?
WHERE meeting_id=? AND note_file_id=? AND status='active' AND version=?`,
fileID, fileName, mimeType, fileSize, fileETag, idempotencyKey, time.Now().UTC().Round(time.Millisecond), meetingID, noteFileID, version)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return nil, ErrConflict
}
return s.meetingNoteFile(ctx, meetingID, noteFileID)
}
func (s *Store) DeleteMeetingNoteFile(ctx context.Context, userID int64, meetingID, noteFileID, idempotencyKey string, version int) (*MeetingNoteFile, error) {
meeting, err := s.Meeting(ctx, userID, meetingID)
if err != nil {
return nil, err
}
if meeting.RecorderID != userID && meeting.OrganizerID != userID {
return nil, ErrForbidden
}
res, err := s.db.ExecContext(ctx, `UPDATE im_meeting_note_files SET status='deleted',version=version+1,idempotency_key=?,updated_at=?
WHERE meeting_id=? AND note_file_id=? AND status='active' AND version=?`,
idempotencyKey, time.Now().UTC().Round(time.Millisecond), meetingID, noteFileID, version)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return nil, ErrConflict
}
return s.meetingNoteFile(ctx, meetingID, noteFileID)
}
func (s *Store) DailyReports(ctx context.Context, userID int64, cursor string, limit int) ([]DailyReport, string, error) {
var items []DailyReport
query := `SELECT report_id,author_id,report_date,content,attachments,status,read_state,version,submitted_at,created_at,updated_at FROM im_daily_reports WHERE author_id=?`
args := []any{userID}
if cursor != "" {
query += ` AND report_id>?`
args = append(args, cursor)
}
query += ` ORDER BY report_date DESC,id DESC LIMIT ?`
args = append(args, limit+1)
if err := s.db.SelectContext(ctx, &items, query, args...); err != nil {
return nil, "", err
}
next := ""
if len(items) > limit {
next = items[limit-1].ReportID
items = items[:limit]
}
return items, next, nil
}
func (s *Store) CreateDailyDraft(ctx context.Context, userID int64, reportID string, reportDate time.Time) (*DailyReport, error) {
now := time.Now().UTC().Round(time.Millisecond)
_, err := s.db.ExecContext(ctx, `INSERT INTO im_daily_reports(report_id,author_id,report_date,content,attachments,status,read_state,version,created_at,updated_at)
VALUES(?,?,?,'','[]','draft','unread',1,?,?)`, reportID, userID, reportDate, now, now)
if isDuplicate(err) {
return nil, ErrConflict
}
if err != nil {
return nil, err
}
return s.DailyReport(ctx, userID, reportID)
}
func (s *Store) DailyReport(ctx context.Context, userID int64, reportID string) (*DailyReport, error) {
var report DailyReport
if err := s.db.GetContext(ctx, &report, `SELECT report_id,author_id,report_date,content,attachments,status,read_state,version,submitted_at,created_at,updated_at
FROM im_daily_reports WHERE report_id=? LIMIT 1`, reportID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
if report.AuthorID != userID {
return nil, ErrForbidden
}
return &report, nil
}
func (s *Store) SaveDailyDraft(ctx context.Context, userID int64, reportID, content string, attachments []string, version int) (*DailyReport, error) {
data, _ := json.Marshal(attachments)
res, err := s.db.ExecContext(ctx, `UPDATE im_daily_reports SET content=?,attachments=?,version=version+1,updated_at=? WHERE report_id=? AND author_id=? AND version=?`,
content, string(data), time.Now().UTC().Round(time.Millisecond), reportID, userID, version)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return nil, ErrConflict
}
return s.DailyReport(ctx, userID, reportID)
}
func (s *Store) SubmitDaily(ctx context.Context, userID int64, reportID string, version int) (*DailyReport, error) {
now := time.Now().UTC().Round(time.Millisecond)
res, err := s.db.ExecContext(ctx, `UPDATE im_daily_reports SET status='submitted',submitted_at=?,version=version+1,updated_at=? WHERE report_id=? AND author_id=? AND version=?`, now, now, reportID, userID, version)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return nil, ErrConflict
}
return s.DailyReport(ctx, userID, reportID)
}
func (s *Store) MonthlyReports(ctx context.Context, userID int64, cursor string, limit int) ([]MonthlyReport, string, error) {
var items []MonthlyReport
query := `SELECT report_id,author_id,report_month,work_summary,next_plan,attachments,status,read_state,version,submitted_at,created_at,updated_at FROM im_monthly_reports WHERE author_id=?`
args := []any{userID}
if cursor != "" {
query += ` AND report_id>?`
args = append(args, cursor)
}
query += ` ORDER BY report_month DESC,id DESC LIMIT ?`
args = append(args, limit+1)
if err := s.db.SelectContext(ctx, &items, query, args...); err != nil {
return nil, "", err
}
next := ""
if len(items) > limit {
next = items[limit-1].ReportID
items = items[:limit]
}
return items, next, nil
}
func (s *Store) CreateMonthlyDraft(ctx context.Context, userID int64, reportID, month string) (*MonthlyReport, error) {
now := time.Now().UTC().Round(time.Millisecond)
_, err := s.db.ExecContext(ctx, `INSERT INTO im_monthly_reports(report_id,author_id,report_month,work_summary,next_plan,attachments,status,read_state,version,created_at,updated_at)
VALUES(?,?,?,'','','[]','draft','unread',1,?,?)`, reportID, userID, month, now, now)
if isDuplicate(err) {
return nil, ErrConflict
}
if err != nil {
return nil, err
}
return s.MonthlyReport(ctx, userID, reportID)
}
func (s *Store) MonthlyReport(ctx context.Context, userID int64, reportID string) (*MonthlyReport, error) {
var report MonthlyReport
if err := s.db.GetContext(ctx, &report, `SELECT report_id,author_id,report_month,work_summary,next_plan,attachments,status,read_state,version,submitted_at,created_at,updated_at
FROM im_monthly_reports WHERE report_id=? LIMIT 1`, reportID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
if report.AuthorID != userID {
return nil, ErrForbidden
}
return &report, nil
}
func (s *Store) SaveMonthlyDraft(ctx context.Context, userID int64, reportID, summary, nextPlan string, attachments []string, version int) (*MonthlyReport, error) {
data, _ := json.Marshal(attachments)
res, err := s.db.ExecContext(ctx, `UPDATE im_monthly_reports SET work_summary=?,next_plan=?,attachments=?,version=version+1,updated_at=? WHERE report_id=? AND author_id=? AND version=?`,
summary, nextPlan, string(data), time.Now().UTC().Round(time.Millisecond), reportID, userID, version)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return nil, ErrConflict
}
return s.MonthlyReport(ctx, userID, reportID)
}
func (s *Store) SubmitMonthly(ctx context.Context, userID int64, reportID string, version int) (*MonthlyReport, error) {
now := time.Now().UTC().Round(time.Millisecond)
res, err := s.db.ExecContext(ctx, `UPDATE im_monthly_reports SET status='submitted',submitted_at=?,version=version+1,updated_at=? WHERE report_id=? AND author_id=? AND version=?`, now, now, reportID, userID, version)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return nil, ErrConflict
}
return s.MonthlyReport(ctx, userID, reportID)
}
func (s *Store) ReopenMonthly(ctx context.Context, userID int64, reportID string, version int) (*MonthlyReport, error) {
res, err := s.db.ExecContext(ctx, `UPDATE im_monthly_reports SET status='draft',version=version+1,updated_at=? WHERE report_id=? AND author_id=? AND version=?`,
time.Now().UTC().Round(time.Millisecond), reportID, userID, version)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return nil, ErrConflict
}
return s.MonthlyReport(ctx, userID, reportID)
}
func (s *Store) MailboxConfig(ctx context.Context) (*MailboxConfig, error) {
var cfg MailboxConfig
if err := s.db.GetContext(ctx, &cfg, `SELECT enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content,CAST(COALESCE(UNIX_TIMESTAMP(notice_updated_at),1) AS UNSIGNED) AS version FROM im_mailbox_config WHERE id=1`); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return &MailboxConfig{Enabled: true, AllowAnonymous: true, RequiredFields: `["subject","content"]`, MaxLength: 5000, AttachmentPolicy: `{"max_files":5}`, NoticeTitle: "总经理信箱", NoticeContent: "请如实填写诉求。", Version: 1}, nil
}
return nil, err
}
return &cfg, nil
}
func (s *Store) CreateMailboxSubmission(ctx context.Context, userID int64, submission MailboxSubmission, idempotencyKey string) (*MailboxSubmission, error) {
now := time.Now().UTC().Round(time.Millisecond)
_, err := s.db.ExecContext(ctx, `INSERT INTO im_mailbox_submissions(submission_id,sender_id,subject,content,anonymous,attachment_file_ids,notice_version,idempotency_key,status,submitted_at)
VALUES(?,?,?,?,?,?,?,?,?,?)`, submission.SubmissionID, userID, submission.Subject, submission.Content, submission.Anonymous, submission.AttachmentFileIDs, submission.NoticeVersion, idempotencyKey, "received", now)
if isDuplicate(err) && idempotencyKey != "" {
var existing MailboxSubmission
if getErr := s.db.GetContext(ctx, &existing, `SELECT submission_id,sender_id,subject,content,anonymous,attachment_file_ids,notice_version,status,submitted_at FROM im_mailbox_submissions WHERE sender_id=? AND idempotency_key=? LIMIT 1`, userID, idempotencyKey); getErr != nil {
return nil, getErr
}
return &existing, nil
}
if err != nil {
return nil, err
}
submission.SenderID = userID
submission.Status = "received"
submission.SubmittedAt = now
return &submission, nil
}
func (s *Store) attendanceRecordByDate(ctx context.Context, userID int64, date time.Time) (*AttendanceRecord, error) {
var record AttendanceRecord
if err := s.db.GetContext(ctx, &record, `SELECT record_id,user_id,record_date,rule_id,shift_id,status,exceptions,appeal_status,version,created_at,updated_at
FROM im_attendance_records WHERE user_id=? AND record_date=? ORDER BY id DESC LIMIT 1`, userID, date.Format("2006-01-02")); err != nil {
return nil, err
}
record.Checkins, _ = s.attendanceCheckins(ctx, record.RecordID)
return &record, nil
}
func (s *Store) attendanceCheckins(ctx context.Context, recordID string) ([]AttendanceCheckin, error) {
var checkins []AttendanceCheckin
err := s.db.SelectContext(ctx, &checkins, `SELECT checkin_id,action,client_time,server_time,result,location,wifi,device FROM im_attendance_checkins WHERE record_id=? ORDER BY id ASC`, recordID)
return checkins, err
}
func (s *Store) attendanceRecordAndCheckinByIdempotency(ctx context.Context, userID int64, key string) (*AttendanceRecord, *AttendanceCheckin, error) {
var checkin AttendanceCheckin
var recordID string
if err := s.db.QueryRowxContext(ctx, `SELECT checkin_id,record_id,action,client_time,server_time,result,location,wifi,device FROM im_attendance_checkins WHERE user_id=? AND idempotency_key=? LIMIT 1`, userID, key).
Scan(&checkin.CheckinID, &recordID, &checkin.Action, &checkin.ClientTime, &checkin.ServerTime, &checkin.Result, &checkin.Location, &checkin.Wifi, &checkin.Device); err != nil {
return nil, nil, err
}
var record AttendanceRecord
if err := s.db.GetContext(ctx, &record, `SELECT record_id,user_id,record_date,rule_id,shift_id,status,exceptions,appeal_status,version,created_at,updated_at FROM im_attendance_records WHERE record_id=?`, recordID); err != nil {
return nil, nil, err
}
record.Checkins, _ = s.attendanceCheckins(ctx, recordID)
return &record, &checkin, nil
}
func (s *Store) meetingParticipants(ctx context.Context, meetingID string) ([]MeetingParticipant, error) {
var items []MeetingParticipant
err := s.db.SelectContext(ctx, &items, `SELECT meeting_id,user_id,response,response_at FROM im_meeting_participants WHERE meeting_id=? ORDER BY id ASC`, meetingID)
return items, err
}
func (s *Store) meetingNoteFile(ctx context.Context, meetingID, noteFileID string) (*MeetingNoteFile, error) {
var file MeetingNoteFile
if err := s.db.GetContext(ctx, &file, `SELECT note_file_id,meeting_id,file_id,uploader_id,file_name,mime_type,file_size,file_etag,status,preview_status,preview_mode,preview_file_id,version,created_at,updated_at
FROM im_meeting_note_files WHERE meeting_id=? AND note_file_id=? LIMIT 1`, meetingID, noteFileID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &file, nil
}
func meetingVisible(meeting Meeting, userID int64) bool {
if meeting.OrganizerID == userID || meeting.RecorderID == userID {
return true
}
for _, participant := range meeting.Participants {
if participant.UserID == userID {
return true
}
}
return false
}
func trimDepartmentPage(items []OrganizationDepartment, limit int) ([]OrganizationDepartment, string, error) {
next := ""
if len(items) > limit {
next = items[limit-1].DepartmentID
items = items[:limit]
}
return items, next, nil
}
func trimMemberPage(items []OrganizationMember, limit int) ([]OrganizationMember, string, error) {
next := ""
if len(items) > limit {
next = fmt.Sprintf("%d", items[limit-1].UserID)
items = items[:limit]
}
return items, next, nil
}
func isDuplicate(err error) bool {
return err != nil && strings.Contains(err.Error(), "Duplicate entry")
}
func isMissingTable(err error) bool {
return err != nil && strings.Contains(err.Error(), "doesn't exist")
}