fix: scope workbench mysql queries by tenant

This commit is contained in:
richjohnrambo
2026-09-22 15:02:09 +08:00
parent 53accc3e3f
commit a39002b49d
2 changed files with 143 additions and 117 deletions
+24 -24
View File
@@ -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
}
+119 -93
View File
@@ -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
}