fix: scope workbench mysql queries by tenant
This commit is contained in:
+24
-24
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user