diff --git a/internal/store/admin.go b/internal/store/admin.go index 907fb7f..843677c 100644 --- a/internal/store/admin.go +++ b/internal/store/admin.go @@ -71,8 +71,8 @@ func (s *Store) AdminWorkbenchModules(ctx context.Context, page, perPage int, co if perPage > 200 { perPage = 200 } - where := []string{"1=1"} - args := []any{} + where := []string{"tenant_id=?"} + args := []any{s.tenant()} if code = strings.TrimSpace(code); code != "" { where = append(where, "code LIKE ?") args = append(args, "%"+code+"%") @@ -110,8 +110,8 @@ func (s *Store) UpsertAdminWorkbenchModule(ctx context.Context, in AdminWorkbenc in.Permission = "enabled" } if in.ID > 0 { - res, err := s.db.ExecContext(ctx, `UPDATE im_workbench_apps SET code=?,name=?,icon_file_id=?,route=?,sort_order=?,unread_key=?,enabled=?,permission=?,updated_at=? WHERE id=?`, - in.Code, in.Name, in.IconFileID, in.Route, in.SortOrder, in.UnreadKey, in.Enabled, in.Permission, now, in.ID) + res, err := s.db.ExecContext(ctx, `UPDATE im_workbench_apps SET code=?,name=?,icon_file_id=?,route=?,sort_order=?,unread_key=?,enabled=?,permission=?,updated_at=? WHERE tenant_id=? AND id=?`, + in.Code, in.Name, in.IconFileID, in.Route, in.SortOrder, in.UnreadKey, in.Enabled, in.Permission, now, s.tenant(), in.ID) if err != nil { return nil, err } @@ -120,8 +120,8 @@ func (s *Store) UpsertAdminWorkbenchModule(ctx context.Context, in AdminWorkbenc } return s.adminWorkbenchModuleByID(ctx, in.ID) } - res, err := s.db.ExecContext(ctx, `INSERT INTO im_workbench_apps(code,name,icon_file_id,route,sort_order,unread_key,enabled,permission,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?,?)`, in.Code, in.Name, in.IconFileID, in.Route, in.SortOrder, in.UnreadKey, in.Enabled, in.Permission, now, now) + res, err := s.db.ExecContext(ctx, `INSERT INTO im_workbench_apps(tenant_id,code,name,icon_file_id,route,sort_order,unread_key,enabled,permission,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?)`, s.tenant(), in.Code, in.Name, in.IconFileID, in.Route, in.SortOrder, in.UnreadKey, in.Enabled, in.Permission, now, now) if err != nil { return nil, err } @@ -134,9 +134,9 @@ func (s *Store) DisableAdminWorkbenchModule(ctx context.Context, id int64, code var res sql.Result var err error if id > 0 { - res, err = s.db.ExecContext(ctx, `UPDATE im_workbench_apps SET enabled=0,updated_at=? WHERE id=?`, now, id) + res, err = s.db.ExecContext(ctx, `UPDATE im_workbench_apps SET enabled=0,updated_at=? WHERE tenant_id=? AND id=?`, now, s.tenant(), id) } else { - res, err = s.db.ExecContext(ctx, `UPDATE im_workbench_apps SET enabled=0,updated_at=? WHERE code=?`, now, strings.TrimSpace(code)) + res, err = s.db.ExecContext(ctx, `UPDATE im_workbench_apps SET enabled=0,updated_at=? WHERE tenant_id=? AND code=?`, now, s.tenant(), strings.TrimSpace(code)) } if err != nil { return err @@ -155,7 +155,7 @@ func (s *Store) ReorderAdminWorkbenchModules(ctx context.Context, items []AdminW defer func() { _ = tx.Rollback() }() now := time.Now().UTC().Round(time.Millisecond) for _, item := range items { - if _, err = tx.ExecContext(ctx, `UPDATE im_workbench_apps SET sort_order=?,updated_at=? WHERE code=?`, item.SortOrder, now, strings.TrimSpace(item.Code)); err != nil { + if _, err = tx.ExecContext(ctx, `UPDATE im_workbench_apps SET sort_order=?,updated_at=? WHERE tenant_id=? AND code=?`, item.SortOrder, now, s.tenant(), strings.TrimSpace(item.Code)); err != nil { return err } } @@ -164,8 +164,8 @@ func (s *Store) ReorderAdminWorkbenchModules(ctx context.Context, items []AdminW func (s *Store) AdminOrganizationDepartments(ctx context.Context, page, perPage int, parentID, name, state string) ([]OrganizationDepartment, int, error) { page, perPage = normalizeAdminPage(page, perPage) - where := []string{"1=1"} - args := []any{} + where := []string{"tenant_id=?"} + args := []any{s.tenant()} if parentID = strings.TrimSpace(parentID); parentID != "" { where = append(where, "parent_id=?") args = append(args, parentID) @@ -187,8 +187,8 @@ func (s *Store) AdminOrganizationDepartments(ctx context.Context, page, perPage queryArgs = append(queryArgs, (page-1)*perPage, perPage) var items []OrganizationDepartment err := s.db.SelectContext(ctx, &items, `SELECT d.department_id,d.name,d.parent_id, - (SELECT COUNT(*) FROM im_org_departments c WHERE c.parent_id=d.department_id AND c.state='active') 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, + (SELECT COUNT(*) FROM im_org_departments c WHERE c.tenant_id=d.tenant_id AND c.parent_id=d.department_id AND c.state='active') AS child_count, + (SELECT COUNT(*) FROM im_org_members m WHERE m.tenant_id=d.tenant_id AND m.department_id=d.department_id AND m.employment_state='active') AS member_count, d.version FROM im_org_departments d WHERE `+whereSQL+` ORDER BY d.parent_id ASC,d.version DESC,d.department_id ASC LIMIT ?,?`, queryArgs...) return items, total, err @@ -196,8 +196,8 @@ func (s *Store) AdminOrganizationDepartments(ctx context.Context, page, perPage func (s *Store) AdminOrganizationMembers(ctx context.Context, page, perPage int, departmentID, keyword, state string, userID int64) ([]OrganizationMember, int, error) { page, perPage = normalizeAdminPage(page, perPage) - where := []string{"1=1"} - args := []any{} + where := []string{"tenant_id=?"} + args := []any{s.tenant()} if departmentID = strings.TrimSpace(departmentID); departmentID != "" { where = append(where, "department_id=?") args = append(args, departmentID) @@ -231,7 +231,7 @@ func (s *Store) AdminOrganizationMembers(ctx context.Context, page, perPage int, func (s *Store) AdminMailboxConfig(ctx context.Context) (*AdminMailboxConfig, error) { var cfg AdminMailboxConfig - if err := s.db.GetContext(ctx, &cfg, `SELECT id,enabled+0 AS enabled,allow_anonymous+0 AS allow_anonymous,CAST(required_fields AS CHAR) AS required_fields,max_length,CAST(attachment_policy AS CHAR) AS attachment_policy,notice_title,notice_content,notice_updated_at,updated_at FROM im_mailbox_config WHERE id=1`); err != nil { + if err := s.db.GetContext(ctx, &cfg, `SELECT id,enabled+0 AS enabled,allow_anonymous+0 AS allow_anonymous,CAST(required_fields AS CHAR) AS required_fields,max_length,CAST(attachment_policy AS CHAR) AS attachment_policy,notice_title,notice_content,notice_updated_at,updated_at FROM im_mailbox_config WHERE tenant_id=? AND id=1`, s.tenant()); err != nil { if errorsIsNoRows(err) { now := time.Now().UTC().Round(time.Millisecond) return &AdminMailboxConfig{ID: 1, Enabled: 1, AllowAnonymous: 1, RequiredFields: `["subject","content"]`, MaxLength: 5000, AttachmentPolicy: `{"max_files":5,"purpose":"executive_mailbox"}`, NoticeTitle: "总经理信箱", NoticeContent: "请如实填写诉求,匿名提交将按企业政策保护展示身份。", NoticeUpdatedAt: now, UpdatedAt: now}, nil @@ -243,16 +243,16 @@ func (s *Store) AdminMailboxConfig(ctx context.Context) (*AdminMailboxConfig, er func (s *Store) UpdateAdminMailboxConfig(ctx context.Context, cfg AdminMailboxConfig) error { now := time.Now().UTC().Round(time.Millisecond) - _, err := s.db.ExecContext(ctx, `INSERT INTO im_mailbox_config(id,enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content,notice_updated_at,created_at,updated_at) - VALUES(1,?,?,?,?,?,?,?,?,?,?) + _, err := s.db.ExecContext(ctx, `INSERT INTO im_mailbox_config(tenant_id,id,enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content,notice_updated_at,created_at,updated_at) + VALUES(?,1,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE enabled=VALUES(enabled),allow_anonymous=VALUES(allow_anonymous),required_fields=VALUES(required_fields),max_length=VALUES(max_length),attachment_policy=VALUES(attachment_policy),notice_title=VALUES(notice_title),notice_content=VALUES(notice_content),notice_updated_at=VALUES(notice_updated_at),updated_at=VALUES(updated_at)`, - cfg.Enabled, cfg.AllowAnonymous, defaultString(cfg.RequiredFields, `["subject","content"]`), cfg.MaxLength, defaultString(cfg.AttachmentPolicy, `{"max_files":5,"purpose":"executive_mailbox"}`), cfg.NoticeTitle, cfg.NoticeContent, now, now, now) + s.tenant(), cfg.Enabled, cfg.AllowAnonymous, defaultString(cfg.RequiredFields, `["subject","content"]`), cfg.MaxLength, defaultString(cfg.AttachmentPolicy, `{"max_files":5,"purpose":"executive_mailbox"}`), cfg.NoticeTitle, cfg.NoticeContent, now, now, now) return err } func (s *Store) AdminFeedbackConfig(ctx context.Context) (*AdminFeedbackConfig, error) { var cfg AdminFeedbackConfig - if err := s.db.GetContext(ctx, &cfg, `SELECT id,CAST(categories AS CHAR) AS categories,max_length,CAST(attachment_policy AS CHAR) AS attachment_policy,allow_contact+0 AS allow_contact,diagnostics_notice,updated_at FROM im_feedback_config WHERE id=1`); err != nil { + if err := s.db.GetContext(ctx, &cfg, `SELECT id,CAST(categories AS CHAR) AS categories,max_length,CAST(attachment_policy AS CHAR) AS attachment_policy,allow_contact+0 AS allow_contact,diagnostics_notice,updated_at FROM im_feedback_config WHERE tenant_id=? AND id=1`, s.tenant()); err != nil { if errorsIsNoRows(err) { return &AdminFeedbackConfig{ID: 1, Categories: `["bug","suggestion","complaint","other"]`, MaxLength: 1000, AttachmentPolicy: `{"max_files":5,"purpose":"feedback"}`, AllowContact: 1, DiagnosticsNotice: "仅在你同意时上传脱敏诊断信息。", UpdatedAt: time.Now().UTC().Round(time.Millisecond)}, nil } @@ -263,16 +263,16 @@ func (s *Store) AdminFeedbackConfig(ctx context.Context) (*AdminFeedbackConfig, func (s *Store) UpdateAdminFeedbackConfig(ctx context.Context, cfg AdminFeedbackConfig) error { now := time.Now().UTC().Round(time.Millisecond) - _, err := s.db.ExecContext(ctx, `INSERT INTO im_feedback_config(id,categories,max_length,attachment_policy,allow_contact,diagnostics_notice,created_at,updated_at) - VALUES(1,?,?,?,?,?,?,?) + _, err := s.db.ExecContext(ctx, `INSERT INTO im_feedback_config(tenant_id,id,categories,max_length,attachment_policy,allow_contact,diagnostics_notice,created_at,updated_at) + VALUES(?,1,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE categories=VALUES(categories),max_length=VALUES(max_length),attachment_policy=VALUES(attachment_policy),allow_contact=VALUES(allow_contact),diagnostics_notice=VALUES(diagnostics_notice),updated_at=VALUES(updated_at)`, - defaultString(cfg.Categories, `["bug","suggestion","complaint","other"]`), cfg.MaxLength, defaultString(cfg.AttachmentPolicy, `{"max_files":5,"purpose":"feedback"}`), cfg.AllowContact, cfg.DiagnosticsNotice, now, now) + s.tenant(), defaultString(cfg.Categories, `["bug","suggestion","complaint","other"]`), cfg.MaxLength, defaultString(cfg.AttachmentPolicy, `{"max_files":5,"purpose":"feedback"}`), cfg.AllowContact, cfg.DiagnosticsNotice, now, now) return err } func (s *Store) adminWorkbenchModuleByID(ctx context.Context, id int64) (*AdminWorkbenchModule, error) { var item AdminWorkbenchModule - if err := s.db.GetContext(ctx, &item, `SELECT id,code,name,IFNULL(icon_file_id,'') AS icon_file_id,route,sort_order,IFNULL(unread_key,'') AS unread_key,enabled+0 AS enabled,IFNULL(permission,'enabled') AS permission,created_at,updated_at FROM im_workbench_apps WHERE id=?`, id); err != nil { + if err := s.db.GetContext(ctx, &item, `SELECT id,code,name,IFNULL(icon_file_id,'') AS icon_file_id,route,sort_order,IFNULL(unread_key,'') AS unread_key,enabled+0 AS enabled,IFNULL(permission,'enabled') AS permission,created_at,updated_at FROM im_workbench_apps WHERE tenant_id=? AND id=?`, s.tenant(), id); err != nil { if errorsIsNoRows(err) { return nil, ErrNotFound } diff --git a/internal/store/mysql.go b/internal/store/mysql.go index 97b9dc5..249ab58 100644 --- a/internal/store/mysql.go +++ b/internal/store/mysql.go @@ -25,10 +25,13 @@ import ( type Store struct { db *sqlx.DB dsn string + tenantID int64 tenantStores map[int64]*Store tenantStoreMu sync.Mutex } +const defaultTenantID int64 = 1 + var ( uidCipherMu sync.RWMutex uidCipher *xtea.Cipher @@ -81,13 +84,20 @@ func openDSN(dsn string) (*Store, error) { return &Store{db: db, dsn: dsn}, nil } +func (s *Store) tenant() int64 { + if s != nil && s.tenantID > 0 { + return s.tenantID + } + return defaultTenantID +} + func (s *Store) Close() error { if s == nil || s.db == nil { return nil } s.tenantStoreMu.Lock() for id, st := range s.tenantStores { - if st != nil && st != s { + if st != nil && st != s && st.db != s.db { _ = st.Close() } delete(s.tenantStores, id) @@ -119,17 +129,13 @@ func (s *Store) ForTenant(ctx context.Context, tenantID int64, tenantCode string if cfg.State != 1 { return nil, fmt.Errorf("tenant disabled: id=%d code=%s", cfg.ID, cfg.Code) } - dsn, err := tenantDSN(s.dsn, cfg.DBName, cfg.DBDSN) - if err != nil { - return nil, err - } - if dsn == "" || dsn == s.dsn { - return s, nil - } cacheID := cfg.ID if cacheID <= 0 { cacheID = tenantID } + if s.tenantID == cacheID { + return s, nil + } s.tenantStoreMu.Lock() if st := s.tenantStores[cacheID]; st != nil { s.tenantStoreMu.Unlock() @@ -137,10 +143,26 @@ func (s *Store) ForTenant(ctx context.Context, tenantID int64, tenantCode string } s.tenantStoreMu.Unlock() + dsn, err := tenantDSN(s.dsn, cfg.DBName, cfg.DBDSN) + if err != nil { + return nil, err + } + if dsn == "" || dsn == s.dsn { + st := &Store{db: s.db, dsn: s.dsn, tenantID: cacheID} + s.tenantStoreMu.Lock() + if existing := s.tenantStores[cacheID]; existing != nil { + s.tenantStoreMu.Unlock() + return existing, nil + } + s.tenantStores[cacheID] = st + s.tenantStoreMu.Unlock() + return st, nil + } st, err := openDSN(dsn) if err != nil { return nil, err } + st.tenantID = cacheID s.tenantStoreMu.Lock() if existing := s.tenantStores[cacheID]; existing != nil { s.tenantStoreMu.Unlock() @@ -195,7 +217,7 @@ func tenantDSN(baseDSN, dbName, dbDSN string) (string, error) { 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`) + FROM im_workbench_apps WHERE tenant_id=? AND enabled=1 ORDER BY sort_order ASC,id ASC`, s.tenant()) if err != nil { return nil, 0, err } @@ -211,7 +233,7 @@ func (s *Store) WorkbenchModules(ctx context.Context, userID int64) ([]Workbench 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) + WHERE tenant_id=? AND user_id=? AND read_at IS NULL AND event_type LIKE 'meeting.%'`, s.tenant(), userID) if isMissingTable(err) { return 0, nil } @@ -224,11 +246,11 @@ func (s *Store) Departments(ctx context.Context, userID int64, parentID, cursor } 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, + (SELECT COUNT(*) FROM im_org_departments c WHERE c.tenant_id=d.tenant_id AND c.parent_id=d.department_id) AS child_count, + (SELECT COUNT(*) FROM im_org_members m WHERE m.tenant_id=d.tenant_id AND 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} + FROM im_org_departments d WHERE d.tenant_id=? AND d.parent_id=?` + args := []any{s.tenant(), parentID} if cursor != "" { query += ` AND d.department_id>?` args = append(args, cursor) @@ -245,8 +267,8 @@ func (s *Store) DepartmentMembers(ctx context.Context, userID int64, departmentI 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} + FROM im_org_members WHERE tenant_id=? AND department_id=? AND employment_state='active'` + args := []any{s.tenant(), departmentID} if cursor != "" { query += ` AND user_id>?` args = append(args, cursor) @@ -261,8 +283,8 @@ func (s *Store) DepartmentMembers(ctx context.Context, userID int64, departmentI 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{} + where := []string{"tenant_id=?", "employment_state='active'"} + args := []any{s.tenant()} if departmentID != "" { where = append(where, "department_id=?") args = append(args, departmentID) @@ -289,7 +311,7 @@ func (s *Store) Members(ctx context.Context, userID int64, departmentID, keyword 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 { + FROM im_attendance_rules WHERE tenant_id=? AND state='active' ORDER BY id ASC LIMIT 1`, s.tenant()); err != nil { if errors.Is(err, sql.ErrNoRows) { return &AttendanceConfig{Date: date, Timezone: "Asia/Shanghai", Rule: AttendanceRule{}, Shifts: []AttendanceShift{}}, nil } @@ -297,7 +319,7 @@ func (s *Store) AttendanceConfig(ctx context.Context, userID int64, date time.Ti } 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 { + FROM im_attendance_shifts WHERE tenant_id=? AND rule_id=? AND state='active' ORDER BY sort_order ASC,id ASC`, s.tenant(), rule.RuleID); err != nil { return nil, err } record, _ := s.attendanceRecordByDate(ctx, userID, date) @@ -305,8 +327,8 @@ func (s *Store) AttendanceConfig(ctx context.Context, userID int64, date time.Ti } func (s *Store) AttendanceRecords(ctx context.Context, userID int64, opts AttendanceListOptions) ([]AttendanceRecord, string, *AttendanceStats, error) { - where := []string{"1=1"} - args := []any{} + where := []string{"tenant_id=?"} + args := []any{s.tenant()} if opts.UserID > 0 { where = append(where, "user_id=?") args = append(args, opts.UserID) @@ -377,16 +399,16 @@ func (s *Store) CreateAttendanceCheckin(ctx context.Context, userID int64, check 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,?,?) + _, err = tx.ExecContext(ctx, `INSERT INTO im_attendance_records(tenant_id,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) + s.tenant(), 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) + _, err = tx.ExecContext(ctx, `INSERT INTO im_attendance_checkins(tenant_id,checkin_id,record_id,user_id,record_date,action,rule_id,shift_id,client_time,server_time,result,location,wifi,device,idempotency_key) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + s.tenant(), 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) @@ -413,8 +435,8 @@ type AttendanceCheckinRequest struct { } func (s *Store) AttendanceAppeals(ctx context.Context, userID int64, status, cursor string, limit int) ([]AttendanceAppeal, string, error) { - where := []string{"user_id=?"} - args := []any{userID} + where := []string{"tenant_id=?", "user_id=?"} + args := []any{s.tenant(), userID} if status != "" { where = append(where, "status=?") args = append(args, status) @@ -443,13 +465,13 @@ func (s *Store) CreateAttendanceAppeal(ctx context.Context, userID int64, appeal 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) + _, err := s.db.ExecContext(ctx, `INSERT INTO im_attendance_appeals(tenant_id,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,?,?,?,?)`, + s.tenant(), 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 { + FROM im_attendance_appeals WHERE tenant_id=? AND user_id=? AND idempotency_key=? LIMIT 1`, s.tenant(), userID, idempotencyKey); getErr != nil { return nil, getErr } return &existing, nil @@ -463,8 +485,8 @@ func (s *Store) CreateAttendanceAppeal(ctx context.Context, userID int64, appeal 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} + FROM im_meetings WHERE tenant_id=? AND (organizer_id=? OR EXISTS(SELECT 1 FROM im_meeting_participants p WHERE p.tenant_id=im_meetings.tenant_id AND p.meeting_id=im_meetings.meeting_id AND p.user_id=?))` + args := []any{s.tenant(), userID, userID} if cursor != "" { query += ` AND meeting_id>?` args = append(args, cursor) @@ -489,7 +511,7 @@ func (s *Store) Meetings(ctx context.Context, userID int64, cursor string, limit 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 { + FROM im_meetings WHERE tenant_id=? AND meeting_id=? LIMIT 1`, s.tenant(), meetingID); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } @@ -520,12 +542,12 @@ func (s *Store) CreateMeeting(ctx context.Context, userID int64, meeting Meeting 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) + _, err = tx.ExecContext(ctx, `INSERT INTO im_meetings(tenant_id,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,?,?)`, s.tenant(), 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 { + if getErr := s.db.GetContext(ctx, &existingID, `SELECT meeting_id FROM im_meetings WHERE tenant_id=? AND organizer_id=? AND idempotency_key=? LIMIT 1`, s.tenant(), userID, meeting.IdempotencyKey); getErr != nil { return nil, getErr } return s.Meeting(ctx, userID, existingID) @@ -540,7 +562,7 @@ func (s *Store) CreateMeeting(ctx context.Context, userID int64, meeting Meeting continue } seen[uid] = true - _, err = tx.ExecContext(ctx, `INSERT INTO im_meeting_participants(meeting_id,user_id,response) VALUES(?,?,?)`, meeting.MeetingID, uid, "pending") + _, err = tx.ExecContext(ctx, `INSERT INTO im_meeting_participants(tenant_id,meeting_id,user_id,response) VALUES(?,?,?,?)`, s.tenant(), meeting.MeetingID, uid, "pending") if err != nil { return nil, err } @@ -569,8 +591,8 @@ func (s *Store) UpdateMeeting(ctx context.Context, userID int64, meetingID strin 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 { + args = append(args, s.tenant(), meetingID) + if _, err := s.db.ExecContext(ctx, `UPDATE im_meetings SET `+strings.Join(sets, ",")+` WHERE tenant_id=? AND meeting_id=?`, args...); err != nil { return nil, err } return s.Meeting(ctx, userID, meetingID) @@ -584,7 +606,7 @@ type MeetingPatch struct { 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 { + if _, err := s.db.ExecContext(ctx, `UPDATE im_meetings SET state='canceled',canceled_at=?,version=version+1,updated_at=? WHERE tenant_id=? AND meeting_id=? AND organizer_id=? AND state='scheduled'`, now, now, s.tenant(), meetingID, userID); err != nil { return nil, err } return s.Meeting(ctx, userID, meetingID) @@ -592,7 +614,7 @@ func (s *Store) CancelMeeting(ctx context.Context, userID int64, meetingID strin 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 { + if _, err := s.db.ExecContext(ctx, `UPDATE im_meeting_participants SET response=?,response_at=? WHERE tenant_id=? AND meeting_id=? AND user_id=?`, response, now, s.tenant(), meetingID, userID); err != nil { return nil, err } return s.Meeting(ctx, userID, meetingID) @@ -619,7 +641,7 @@ func (s *Store) MeetingNoteFiles(ctx context.Context, userID int64, meetingID st } 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 { + FROM im_meeting_note_files WHERE tenant_id=? AND meeting_id=? AND status='active' ORDER BY id DESC`, s.tenant(), meetingID); err != nil { return nil, err } if err := s.hydrateMeetingNoteFilesUploaders(ctx, files); err != nil { @@ -640,12 +662,12 @@ func (s *Store) CreateMeetingNoteFile(ctx context.Context, userID int64, meeting fileID := strings.TrimSpace(input.FileID) meta := s.fileUploadMetadata(ctx, fileID) fileName, mimeType, fileSize, fileETag := normalizeMeetingNoteFileMetadata(input, meta, fileID) - _, 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) + _, err = s.db.ExecContext(ctx, `INSERT INTO im_meeting_note_files(tenant_id,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,?,?,?)`, s.tenant(), 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 { + FROM im_meeting_note_files WHERE tenant_id=? AND meeting_id=? AND uploader_id=? AND idempotency_key=? LIMIT 1`, s.tenant(), meetingID, userID, idempotencyKey); getErr != nil { return nil, getErr } return &existing, nil @@ -655,7 +677,7 @@ func (s *Store) CreateMeetingNoteFile(ctx context.Context, userID int64, meeting } 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 { + FROM im_meeting_note_files WHERE tenant_id=? AND note_file_id=? LIMIT 1`, s.tenant(), noteFileID); err != nil { return nil, err } return &created, nil @@ -673,8 +695,8 @@ func (s *Store) ReplaceMeetingNoteFile(ctx context.Context, userID int64, meetin meta := s.fileUploadMetadata(ctx, fileID) fileName, mimeType, fileSize, fileETag := normalizeMeetingNoteFileMetadata(input, meta, fileID) 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) + WHERE tenant_id=? AND meeting_id=? AND note_file_id=? AND status='active' AND version=?`, + fileID, fileName, mimeType, fileSize, fileETag, idempotencyKey, time.Now().UTC().Round(time.Millisecond), s.tenant(), meetingID, noteFileID, version) if err != nil { return nil, err } @@ -699,14 +721,14 @@ func (s *Store) fileUploadMetadata(ctx context.Context, fileID string) fileUploa return meta } if decodedID > 0 { - err := s.db.GetContext(ctx, &meta, `SELECT IFNULL(mimetype,'') AS mimetype,IFNULL(size,0) AS size,IFNULL(etag,'') AS etag,IFNULL(location,'') AS location FROM im_fileuploads WHERE id=? LIMIT 1`, decodedID) + err := s.db.GetContext(ctx, &meta, `SELECT IFNULL(mimetype,'') AS mimetype,IFNULL(size,0) AS size,IFNULL(etag,'') AS etag,IFNULL(location,'') AS location FROM im_fileuploads WHERE tenant_id=? AND id=? LIMIT 1`, s.tenant(), decodedID) if err == nil { return meta } } for _, candidate := range locationCandidates { err := s.db.GetContext(ctx, &meta, `SELECT IFNULL(mimetype,'') AS mimetype,IFNULL(size,0) AS size,IFNULL(etag,'') AS etag,IFNULL(location,'') AS location - FROM im_fileuploads WHERE location=? OR location LIKE ? ORDER BY createdat DESC LIMIT 1`, candidate, "%/"+candidate) + FROM im_fileuploads WHERE tenant_id=? AND (location=? OR location LIKE ?) ORDER BY createdat DESC LIMIT 1`, s.tenant(), candidate, "%/"+candidate) if err == nil { return meta } @@ -867,8 +889,8 @@ func (s *Store) DeleteMeetingNoteFile(ctx context.Context, userID int64, meeting 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) + WHERE tenant_id=? AND meeting_id=? AND note_file_id=? AND status='active' AND version=?`, + idempotencyKey, time.Now().UTC().Round(time.Millisecond), s.tenant(), meetingID, noteFileID, version) if err != nil { return nil, err } @@ -880,8 +902,8 @@ func (s *Store) DeleteMeetingNoteFile(ctx context.Context, userID int64, meeting 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} + 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 tenant_id=? AND author_id=?` + args := []any{s.tenant(), userID} if cursor != "" { query += ` AND report_id>?` args = append(args, cursor) @@ -901,8 +923,8 @@ func (s *Store) DailyReports(ctx context.Context, userID int64, cursor string, l 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) + _, err := s.db.ExecContext(ctx, `INSERT INTO im_daily_reports(tenant_id,report_id,author_id,report_date,content,attachments,status,read_state,version,created_at,updated_at) + VALUES(?,?,?,?,'','[]','draft','unread',1,?,?)`, s.tenant(), reportID, userID, reportDate, now, now) if isDuplicate(err) { return nil, ErrConflict } @@ -915,7 +937,7 @@ func (s *Store) CreateDailyDraft(ctx context.Context, userID int64, reportID str 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 { + FROM im_daily_reports WHERE tenant_id=? AND report_id=? LIMIT 1`, s.tenant(), reportID); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } @@ -929,8 +951,8 @@ func (s *Store) DailyReport(ctx context.Context, userID int64, reportID string) 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) + res, err := s.db.ExecContext(ctx, `UPDATE im_daily_reports SET content=?,attachments=?,version=version+1,updated_at=? WHERE tenant_id=? AND report_id=? AND author_id=? AND version=?`, + content, string(data), time.Now().UTC().Round(time.Millisecond), s.tenant(), reportID, userID, version) if err != nil { return nil, err } @@ -942,7 +964,7 @@ func (s *Store) SaveDailyDraft(ctx context.Context, userID int64, reportID, cont 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) + res, err := s.db.ExecContext(ctx, `UPDATE im_daily_reports SET status='submitted',submitted_at=?,version=version+1,updated_at=? WHERE tenant_id=? AND report_id=? AND author_id=? AND version=?`, now, now, s.tenant(), reportID, userID, version) if err != nil { return nil, err } @@ -955,6 +977,8 @@ func (s *Store) SubmitDaily(ctx context.Context, userID int64, reportID string, func (s *Store) DailyReportStats(ctx context.Context, date time.Time, departmentID string) (*ReportStats, error) { stats := &ReportStats{DepartmentID: strings.TrimSpace(departmentID)} memberWhere, memberArgs := reportMemberScope(stats.DepartmentID) + memberWhere = append([]string{"tenant_id=?"}, memberWhere...) + memberArgs = append([]any{s.tenant()}, memberArgs...) if err := s.db.GetContext(ctx, &stats.ExpectedCount, `SELECT COUNT(DISTINCT user_id) FROM im_org_members WHERE `+strings.Join(memberWhere, " AND "), memberArgs...); err != nil { if isMissingTable(err) { @@ -962,12 +986,12 @@ func (s *Store) DailyReportStats(ctx context.Context, date time.Time, department } return nil, err } - args := append([]any{date.Format("2006-01-02")}, memberArgs...) + args := append([]any{s.tenant(), date.Format("2006-01-02")}, memberArgs...) if err := s.db.GetContext(ctx, &stats.SubmittedCount, `SELECT COUNT(DISTINCT r.author_id) FROM im_daily_reports AS r - INNER JOIN im_org_members AS m ON m.user_id=r.author_id - WHERE r.report_date=? AND r.status='submitted' AND `+strings.Join(prefixReportMemberScope(memberWhere), " AND "), args...); err != nil { + INNER JOIN im_org_members AS m ON m.tenant_id=r.tenant_id AND m.user_id=r.author_id + WHERE r.tenant_id=? AND r.report_date=? AND r.status='submitted' AND `+strings.Join(prefixReportMemberScope(memberWhere), " AND "), args...); err != nil { if isMissingTable(err) { return stats, nil } @@ -982,8 +1006,8 @@ func (s *Store) DailyReportStats(ctx context.Context, date time.Time, department 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} + 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 tenant_id=? AND author_id=?` + args := []any{s.tenant(), userID} if cursor != "" { query += ` AND report_id>?` args = append(args, cursor) @@ -1003,8 +1027,8 @@ func (s *Store) MonthlyReports(ctx context.Context, userID int64, cursor string, 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) + _, err := s.db.ExecContext(ctx, `INSERT INTO im_monthly_reports(tenant_id,report_id,author_id,report_month,work_summary,next_plan,attachments,status,read_state,version,created_at,updated_at) + VALUES(?,?,?,?,'','','[]','draft','unread',1,?,?)`, s.tenant(), reportID, userID, month, now, now) if isDuplicate(err) { return nil, ErrConflict } @@ -1017,7 +1041,7 @@ func (s *Store) CreateMonthlyDraft(ctx context.Context, userID int64, 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 { + FROM im_monthly_reports WHERE tenant_id=? AND report_id=? LIMIT 1`, s.tenant(), reportID); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } @@ -1031,8 +1055,8 @@ func (s *Store) MonthlyReport(ctx context.Context, userID int64, reportID string 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) + res, err := s.db.ExecContext(ctx, `UPDATE im_monthly_reports SET work_summary=?,next_plan=?,attachments=?,version=version+1,updated_at=? WHERE tenant_id=? AND report_id=? AND author_id=? AND version=?`, + summary, nextPlan, string(data), time.Now().UTC().Round(time.Millisecond), s.tenant(), reportID, userID, version) if err != nil { return nil, err } @@ -1044,7 +1068,7 @@ func (s *Store) SaveMonthlyDraft(ctx context.Context, userID int64, reportID, su 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) + res, err := s.db.ExecContext(ctx, `UPDATE im_monthly_reports SET status='submitted',submitted_at=?,version=version+1,updated_at=? WHERE tenant_id=? AND report_id=? AND author_id=? AND version=?`, now, now, s.tenant(), reportID, userID, version) if err != nil { return nil, err } @@ -1055,8 +1079,8 @@ func (s *Store) SubmitMonthly(ctx context.Context, userID int64, reportID string } 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) + res, err := s.db.ExecContext(ctx, `UPDATE im_monthly_reports SET status='draft',version=version+1,updated_at=? WHERE tenant_id=? AND report_id=? AND author_id=? AND version=?`, + time.Now().UTC().Round(time.Millisecond), s.tenant(), reportID, userID, version) if err != nil { return nil, err } @@ -1069,6 +1093,8 @@ func (s *Store) ReopenMonthly(ctx context.Context, userID int64, reportID string func (s *Store) MonthlyReportStats(ctx context.Context, month, departmentID string) (*ReportStats, error) { stats := &ReportStats{DepartmentID: strings.TrimSpace(departmentID)} memberWhere, memberArgs := reportMemberScope(stats.DepartmentID) + memberWhere = append([]string{"tenant_id=?"}, memberWhere...) + memberArgs = append([]any{s.tenant()}, memberArgs...) if err := s.db.GetContext(ctx, &stats.ExpectedCount, `SELECT COUNT(DISTINCT user_id) FROM im_org_members WHERE `+strings.Join(memberWhere, " AND "), memberArgs...); err != nil { if isMissingTable(err) { @@ -1076,12 +1102,12 @@ func (s *Store) MonthlyReportStats(ctx context.Context, month, departmentID stri } return nil, err } - args := append([]any{strings.TrimSpace(month)}, memberArgs...) + args := append([]any{s.tenant(), strings.TrimSpace(month)}, memberArgs...) if err := s.db.GetContext(ctx, &stats.SubmittedCount, `SELECT COUNT(DISTINCT r.author_id) FROM im_monthly_reports AS r - INNER JOIN im_org_members AS m ON m.user_id=r.author_id - WHERE r.report_month=? AND r.status='submitted' AND `+strings.Join(prefixReportMemberScope(memberWhere), " AND "), args...); err != nil { + INNER JOIN im_org_members AS m ON m.tenant_id=r.tenant_id AND m.user_id=r.author_id + WHERE r.tenant_id=? AND r.report_month=? AND r.status='submitted' AND `+strings.Join(prefixReportMemberScope(memberWhere), " AND "), args...); err != nil { if isMissingTable(err) { return stats, nil } @@ -1096,7 +1122,7 @@ func (s *Store) MonthlyReportStats(ctx context.Context, month, departmentID stri 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 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 tenant_id=? AND id=1`, s.tenant()); 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 } @@ -1123,11 +1149,11 @@ func (s *Store) CreateMailboxSubmission(ctx context.Context, userID int64, submi return nil, ErrPolicy } 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) + _, err = s.db.ExecContext(ctx, `INSERT INTO im_mailbox_submissions(tenant_id,submission_id,sender_id,subject,content,anonymous,attachment_file_ids,notice_version,idempotency_key,status,submitted_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?)`, s.tenant(), 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 { + 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 tenant_id=? AND sender_id=? AND idempotency_key=? LIMIT 1`, s.tenant(), userID, idempotencyKey); getErr != nil { return nil, getErr } return &existing, nil @@ -1144,7 +1170,7 @@ func (s *Store) CreateMailboxSubmission(ctx context.Context, userID int64, submi 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 { + FROM im_attendance_records WHERE tenant_id=? AND user_id=? AND record_date=? ORDER BY id DESC LIMIT 1`, s.tenant(), userID, date.Format("2006-01-02")); err != nil { return nil, err } record.Checkins, _ = s.attendanceCheckins(ctx, record.RecordID) @@ -1153,19 +1179,19 @@ func (s *Store) attendanceRecordByDate(ctx context.Context, userID int64, date t 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) + err := s.db.SelectContext(ctx, &checkins, `SELECT checkin_id,action,client_time,server_time,result,location,wifi,device FROM im_attendance_checkins WHERE tenant_id=? AND record_id=? ORDER BY id ASC`, s.tenant(), 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). + 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 tenant_id=? AND user_id=? AND idempotency_key=? LIMIT 1`, s.tenant(), 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 { + 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 tenant_id=? AND record_id=?`, s.tenant(), recordID); err != nil { return nil, nil, err } record.Checkins, _ = s.attendanceCheckins(ctx, recordID) @@ -1174,7 +1200,7 @@ func (s *Store) attendanceRecordAndCheckinByIdempotency(ctx context.Context, use 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) + err := s.db.SelectContext(ctx, &items, `SELECT meeting_id,user_id,response,response_at FROM im_meeting_participants WHERE tenant_id=? AND meeting_id=? ORDER BY id ASC`, s.tenant(), meetingID) return items, err } @@ -1215,7 +1241,7 @@ func (s *Store) UserSummaries(ctx context.Context, userIDs []int64) (map[int64]U } query, args, err := sqlx.In(`SELECT user_id,display_name,IFNULL(avatar_file_id,'') AS avatar_file_id - FROM im_org_members WHERE employment_state='active' AND user_id IN (?) ORDER BY user_id ASC,id ASC`, ids) + FROM im_org_members WHERE tenant_id=? AND employment_state='active' AND user_id IN (?) ORDER BY user_id ASC,id ASC`, s.tenant(), ids) if err != nil { return nil, err } @@ -1235,7 +1261,7 @@ func (s *Store) UserSummaries(ctx context.Context, userIDs []int64) (map[int64]U } query, args, err = sqlx.In(`SELECT user_id,nickname AS display_name,IFNULL(avatar_file_id,'') AS avatar_file_id - FROM im_user_profiles WHERE user_id IN (?) ORDER BY user_id ASC`, ids) + FROM im_user_profiles WHERE tenant_id=? AND user_id IN (?) ORDER BY user_id ASC`, s.tenant(), ids) if err != nil { return nil, err } @@ -1269,7 +1295,7 @@ func (s *Store) UserSummaries(ctx context.Context, userIDs []int64) (map[int64]U NULLIF(JSON_UNQUOTE(JSON_EXTRACT(public,'$.name')),''), '' ) AS display_name - FROM im_users WHERE id IN (?)`, ids) + FROM im_users WHERE tenant_id=? AND id IN (?)`, s.tenant(), ids) if err != nil { return nil, err } @@ -1324,7 +1350,7 @@ func summaryOrDefault(userID int64, summaries map[int64]UserSummary) UserSummary 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 { + FROM im_meeting_note_files WHERE tenant_id=? AND meeting_id=? AND note_file_id=? LIMIT 1`, s.tenant(), meetingID, noteFileID); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound }