refactor: remove workbench code from im server

This commit is contained in:
richjohnrambo
2026-09-17 23:38:44 +08:00
parent 69a777c81f
commit 0e2f4e73d1
28 changed files with 79 additions and 9182 deletions
-1
View File
@@ -138,7 +138,6 @@ func TestUserQuerySupportsKeywordAndUserIDs(t *testing.T) {
"im_fileuploads AS avatar",
"JSON_EXTRACT(u.public,'$.avatar')",
"JSON_EXTRACT(u.public,'$.photo.data')",
"FROM im_org_members AS om",
} {
if !strings.Contains(usersInternalQuerySpec().Select+usersInternalQuerySpec().From, field) {
t.Fatalf("user query missing list field %q", field)
-38
View File
@@ -403,44 +403,6 @@ type TenantBusinessAdapter interface {
TenantGroupOwnerTransfer(t.TenantID, string, t.Uid, t.Uid, int, time.Time) error
TenantGroupCanPost(t.TenantID, string, t.Uid, bool, time.Time) (bool, error)
TenantWorkbenchModules(t.TenantID, t.Uid) ([]t.WorkbenchModule, int, error)
TenantOrganizationDepartments(t.TenantID, t.Uid, string, string, int) ([]t.OrganizationDepartment, string, error)
TenantOrganizationMembers(t.TenantID, t.Uid, string, string, int) ([]t.OrganizationMember, string, error)
TenantOrganizationMembersAll(t.TenantID, t.Uid, string, string, string, int) ([]t.OrganizationMember, string, error)
TenantOrganizationSearch(t.TenantID, t.Uid, string, string, string, int) ([]t.OrganizationSearchItem, string, error)
TenantAttendanceConfig(t.TenantID, t.Uid, time.Time) (*t.AttendanceConfig, error)
TenantAttendanceRecordList(t.TenantID, t.Uid, t.AttendanceRecordListOptions) ([]t.AttendanceRecord, string, *t.AttendanceRecordStats, error)
TenantAttendanceCheckin(t.TenantID, t.Uid, string, t.AttendanceCheckinRequest) (*t.AttendanceRecord, *t.AttendanceCheckin, error)
TenantAttendanceAppealCreate(t.TenantID, t.Uid, *t.AttendanceAppeal, string) (*t.AttendanceAppeal, error)
TenantAttendanceAppealList(t.TenantID, t.Uid, t.AttendanceAppealListOptions) ([]t.AttendanceAppeal, string, error)
TenantMeetingCreate(t.TenantID, t.Uid, *t.Meeting, []t.Uid) (*t.Meeting, error)
TenantMeetingList(t.TenantID, t.Uid, string, int) ([]t.Meeting, string, error)
TenantMeetingGet(t.TenantID, t.Uid, string) (*t.Meeting, error)
TenantMeetingUpdate(t.TenantID, t.Uid, string, t.MeetingPatch, string) (*t.Meeting, error)
TenantMeetingCancel(t.TenantID, t.Uid, string, string) (*t.Meeting, error)
TenantMeetingRemind(t.TenantID, t.Uid, string, string) ([]t.MeetingParticipant, error)
TenantMeetingRespond(t.TenantID, t.Uid, string, string, int) (*t.Meeting, error)
TenantMeetingNoteList(t.TenantID, t.Uid, string, string, int) ([]t.Meeting, string, error)
TenantMeetingNoteFiles(t.TenantID, t.Uid, string) ([]t.MeetingNoteFile, error)
TenantMeetingNoteFileCreate(t.TenantID, t.Uid, string, string, string, string) (*t.MeetingNoteFile, error)
TenantMeetingNoteFileReplace(t.TenantID, t.Uid, string, string, string, string, int) (*t.MeetingNoteFile, error)
TenantMeetingNoteFileDelete(t.TenantID, t.Uid, string, string, string, int) (*t.MeetingNoteFile, error)
TenantDailyReportList(t.TenantID, t.Uid, string, string, string, string, string, int) ([]t.DailyReport, string, error)
TenantDailyReportCreateDraft(t.TenantID, t.Uid, string, time.Time) (*t.DailyReport, error)
TenantDailyReportGet(t.TenantID, t.Uid, string) (*t.DailyReport, error)
TenantDailyReportSaveDraft(t.TenantID, t.Uid, string, string, []string, int) (*t.DailyReport, error)
TenantDailyReportSubmit(t.TenantID, t.Uid, string, int) (*t.DailyReport, error)
TenantDailyReportStats(t.TenantID, t.Uid, time.Time, string) (*t.DailyReportStats, error)
TenantDailyReportRemind(t.TenantID, t.Uid, time.Time, []t.Uid, string) ([]t.Uid, error)
TenantMonthlyReportList(t.TenantID, t.Uid, string, string, string, string, string, int) ([]t.MonthlyReport, string, error)
TenantMonthlyReportCreateDraft(t.TenantID, t.Uid, string, string) (*t.MonthlyReport, error)
TenantMonthlyReportGet(t.TenantID, t.Uid, string) (*t.MonthlyReport, error)
TenantMonthlyReportSaveDraft(t.TenantID, t.Uid, string, string, string, []string, int) (*t.MonthlyReport, error)
TenantMonthlyReportSubmit(t.TenantID, t.Uid, string, int) (*t.MonthlyReport, error)
TenantMonthlyReportReopen(t.TenantID, t.Uid, string, int) (*t.MonthlyReport, error)
TenantMonthlyReportStats(t.TenantID, t.Uid, string, string) (*t.MonthlyReportStats, error)
TenantMailboxConfigGet(t.TenantID, t.Uid) (*t.MailboxConfig, error)
TenantMailboxSubmissionCreate(t.TenantID, t.Uid, *t.MailboxSubmission, string) (*t.MailboxSubmission, error)
TenantUserProfileGet(t.TenantID, t.Uid) (*t.UserProfile, error)
TenantUserProfileUpdate(t.TenantID, t.Uid, t.UserProfilePatch) (*t.UserProfile, error)
TenantUserPrivacyGet(t.TenantID, t.Uid) (*t.UserPrivacySettings, error)
+3 -531
View File
@@ -844,495 +844,6 @@ func createIMCallTables(tx *sql.Tx) error {
return err
}
func createIMWorkbenchTables(tx *sql.Tx) error {
if _, err := tx.Exec(
`CREATE TABLE im_workbench_apps(
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
route VARCHAR(128) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
unread_key VARCHAR(64) NOT NULL DEFAULT '',
enabled TINYINT NOT NULL DEFAULT 1,
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_workbench_apps_code(code),
INDEX idx_im_workbench_apps_enabled_order(enabled, sort_order)
)`); err != nil {
return err
}
_, err := tx.Exec(
`INSERT INTO im_workbench_apps(code,name,route,sort_order,unread_key,enabled,permission)
VALUES
('attendance','打卡','/attendance',10,'attendance',1,'enabled'),
('monthly_reports','月报','/reports/monthly',20,'monthly_reports',1,'enabled'),
('daily_reports','日报','/reports/daily',30,'daily_reports',1,'enabled'),
('organization','部门框架','/organization',40,'organization',1,'enabled'),
('executive_mailbox','举报信箱','/executive-mailbox',50,'executive_mailbox',1,'enabled'),
('meetings','会议通知','/meetings',60,'meetings',1,'enabled'),
('meeting_notes','会议笔记','/meeting-notes',70,'',1,'enabled')
ON DUPLICATE KEY UPDATE
name=VALUES(name),
route=VALUES(route),
sort_order=VALUES(sort_order),
unread_key=VALUES(unread_key),
updated_at=CURRENT_TIMESTAMP(3)`)
return err
}
func createIMOrganizationTables(tx *sql.Tx) error {
statements := []string{
`CREATE TABLE im_org_departments(
id BIGINT NOT NULL AUTO_INCREMENT,
department_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
sort_order INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
state VARCHAR(16) NOT NULL DEFAULT 'active',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_org_departments_department(department_id),
INDEX idx_im_org_departments_parent(parent_id, state, sort_order, id),
INDEX idx_im_org_departments_name(name)
)`,
`CREATE TABLE im_org_members(
id BIGINT NOT NULL AUTO_INCREMENT,
department_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
display_name VARCHAR(128) NOT NULL DEFAULT '',
position VARCHAR(128) NOT NULL DEFAULT '',
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
mobile VARCHAR(32) NOT NULL DEFAULT '',
email VARCHAR(128) NOT NULL DEFAULT '',
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
contactable TINYINT NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_org_members_department_user(department_id, user_id),
INDEX idx_im_org_members_department(department_id, employment_state, sort_order, id),
INDEX idx_im_org_members_user(user_id),
INDEX idx_im_org_members_display_name(display_name),
CONSTRAINT fk_im_org_members_department FOREIGN KEY(department_id) REFERENCES im_org_departments(department_id) ON DELETE CASCADE
)`,
`CREATE TABLE im_org_visibility_overrides(
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
visibility_scope VARCHAR(16) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
remark VARCHAR(256) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_org_visibility_user(user_id),
INDEX idx_im_org_visibility_scope(visibility_scope,state)
)`,
}
for _, stmt := range statements {
if _, err := tx.Exec(stmt); err != nil {
return err
}
}
return nil
}
func ensureIMOrganizationVisibilityTables(exec sqlExecutor) error {
_, err := exec.Exec(`CREATE TABLE IF NOT EXISTS im_org_visibility_overrides(
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
visibility_scope VARCHAR(16) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
remark VARCHAR(256) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_org_visibility_user(user_id),
INDEX idx_im_org_visibility_scope(visibility_scope,state)
)`)
return err
}
func createIMAttendanceTables(tx *sql.Tx) error {
for _, stmt := range imAttendanceTableStatements(false) {
if _, err := tx.Exec(stmt); err != nil {
return err
}
}
return seedIMAttendanceDefaults(tx)
}
func ensureIMAttendanceTables(db *sqlx.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
for _, stmt := range imAttendanceTableStatements(true) {
if _, err = tx.Exec(stmt); err != nil {
return err
}
}
if err = seedIMAttendanceDefaults(tx); err != nil {
return err
}
return tx.Commit()
}
func imAttendanceTableStatements(ifNotExists bool) []string {
statements := []string{
`CREATE TABLE im_attendance_rules(
id BIGINT NOT NULL AUTO_INCREMENT,
rule_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
workday TINYINT NOT NULL DEFAULT 1,
location_required TINYINT NOT NULL DEFAULT 0,
wifi_required TINYINT NOT NULL DEFAULT 0,
allow_outside_checkin TINYINT NOT NULL DEFAULT 1,
appeal_allowed TINYINT NOT NULL DEFAULT 1,
effective_from DATE,
effective_to DATE,
state VARCHAR(16) NOT NULL DEFAULT 'active',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_attendance_rules_rule(rule_id),
INDEX idx_im_attendance_rules_state(state,effective_from,effective_to)
)`,
`CREATE TABLE im_attendance_shifts(
id BIGINT NOT NULL AUTO_INCREMENT,
shift_id VARCHAR(64) NOT NULL,
rule_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
work_start TIME NOT NULL,
work_end TIME NOT NULL,
checkin_start TIME NOT NULL,
checkout_end TIME NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_attendance_shifts_shift(shift_id),
INDEX idx_im_attendance_shifts_rule(rule_id,state,sort_order)
)`,
`CREATE TABLE im_attendance_records(
id BIGINT NOT NULL AUTO_INCREMENT,
record_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
record_date DATE NOT NULL,
rule_id VARCHAR(64) NOT NULL,
shift_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'not_checked',
exceptions JSON,
appeal_status VARCHAR(32) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_attendance_records_record(record_id),
UNIQUE INDEX uk_im_attendance_records_user_date_shift(user_id,record_date,shift_id),
INDEX idx_im_attendance_records_user_id(user_id,id DESC),
INDEX idx_im_attendance_records_date_status(record_date,status,id DESC)
)`,
`CREATE TABLE im_attendance_checkins(
id BIGINT NOT NULL AUTO_INCREMENT,
checkin_id VARCHAR(64) NOT NULL,
record_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
record_date DATE NOT NULL,
action VARCHAR(32) NOT NULL,
rule_id VARCHAR(64) NOT NULL,
shift_id VARCHAR(64) NOT NULL,
client_time DATETIME(3) NOT NULL,
server_time DATETIME(3) NOT NULL,
result VARCHAR(32) NOT NULL,
location JSON,
wifi JSON,
device JSON,
idempotency_key VARCHAR(128) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_attendance_checkins_checkin(checkin_id),
UNIQUE INDEX uk_im_attendance_checkins_idempotency(user_id,idempotency_key),
INDEX idx_im_attendance_checkins_record(record_id,id)
)`,
`CREATE TABLE im_attendance_appeals(
id BIGINT NOT NULL AUTO_INCREMENT,
appeal_id VARCHAR(64) NOT NULL,
record_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
appeal_type VARCHAR(64) NOT NULL,
reason VARCHAR(512) NOT NULL,
target_action VARCHAR(32) NOT NULL DEFAULT '',
target_time DATETIME(3),
attachment_file_ids JSON,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
version INT NOT NULL DEFAULT 1,
idempotency_key VARCHAR(128) NOT NULL,
submitted_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_attendance_appeals_appeal(appeal_id),
UNIQUE INDEX uk_im_attendance_appeals_idempotency(user_id,idempotency_key),
INDEX idx_im_attendance_appeals_record(record_id),
INDEX idx_im_attendance_appeals_user_status(user_id,status,id DESC)
)`,
}
if ifNotExists {
for i, stmt := range statements {
statements[i] = strings.Replace(stmt, "CREATE TABLE ", "CREATE TABLE IF NOT EXISTS ", 1)
}
}
return statements
}
func seedIMAttendanceDefaults(tx *sql.Tx) error {
if _, err := tx.Exec(`INSERT INTO im_attendance_rules(rule_id,name,workday,location_required,wifi_required,allow_outside_checkin,appeal_allowed)
VALUES('att_rule_default','默认考勤规则',1,0,0,1,1)
ON DUPLICATE KEY UPDATE updated_at=updated_at`); err != nil {
return err
}
_, err := tx.Exec(`INSERT INTO im_attendance_shifts(shift_id,rule_id,name,work_start,work_end,checkin_start,checkout_end,sort_order)
VALUES('shift_default','att_rule_default','默认班次','09:00:00','18:00:00','08:30:00','20:00:00',10)
ON DUPLICATE KEY UPDATE updated_at=updated_at`)
return err
}
func createIMMeetingTables(tx *sql.Tx) error {
statements := []string{
`CREATE TABLE im_meetings(
id BIGINT NOT NULL AUTO_INCREMENT,
meeting_id VARCHAR(64) NOT NULL,
organizer_id BIGINT NOT NULL,
recorder_user_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
start_at DATETIME(3) NOT NULL,
end_at DATETIME(3) NOT NULL,
location VARCHAR(256) NOT NULL DEFAULT '',
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
reminder_minutes JSON,
idempotency_key VARCHAR(128),
version INT NOT NULL DEFAULT 1,
canceled_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_meetings_meeting(meeting_id),
UNIQUE INDEX uk_im_meetings_idempotency(organizer_id, idempotency_key),
INDEX idx_im_meetings_organizer(organizer_id, start_at),
INDEX idx_im_meetings_recorder(recorder_user_id, end_at),
INDEX idx_im_meetings_state_time(state, start_at)
)`,
`CREATE TABLE im_meeting_participants(
id BIGINT NOT NULL AUTO_INCREMENT,
meeting_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
response VARCHAR(16) NOT NULL DEFAULT 'pending',
response_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_meeting_participants_user(meeting_id, user_id),
INDEX idx_im_meeting_participants_user_time(user_id, meeting_id),
INDEX idx_im_meeting_participants_response(meeting_id, response),
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY(meeting_id) REFERENCES im_meetings(meeting_id) ON DELETE CASCADE
)`,
`CREATE TABLE im_meeting_note_files(
id BIGINT NOT NULL AUTO_INCREMENT,
note_file_id VARCHAR(64) NOT NULL,
meeting_id VARCHAR(64) NOT NULL,
file_id VARCHAR(64) NOT NULL,
uploader_id BIGINT NOT NULL,
file_name VARCHAR(255) NOT NULL DEFAULT '',
mime_type VARCHAR(128) NOT NULL DEFAULT '',
file_size BIGINT NOT NULL DEFAULT 0,
file_etag VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'active',
preview_status VARCHAR(16) NOT NULL DEFAULT 'ready',
preview_mode VARCHAR(16) NOT NULL DEFAULT 'native',
preview_file_id VARCHAR(64) NOT NULL DEFAULT '',
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
deleted_by BIGINT,
deleted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_meeting_note_files_id(note_file_id),
INDEX idx_im_meeting_note_files_file(meeting_id,file_id,status),
UNIQUE INDEX uk_im_meeting_note_files_idempotency(meeting_id,uploader_id,idempotency_key),
INDEX idx_im_meeting_note_files_meeting(meeting_id,status,id DESC),
INDEX idx_im_meeting_note_files_uploader(uploader_id,id DESC)
)`,
}
for _, stmt := range statements {
if _, err := tx.Exec(stmt); err != nil {
return err
}
}
return nil
}
func ensureIMMeetingNotesTables(db *sqlx.DB) error {
if db == nil {
return nil
}
var recorderExists bool
if err := db.QueryRow(`SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE table_schema=DATABASE() AND table_name='im_meetings' AND column_name='recorder_user_id'
)`).Scan(&recorderExists); err != nil {
return err
}
if !recorderExists {
if _, err := db.Exec(`ALTER TABLE im_meetings ADD COLUMN recorder_user_id BIGINT NOT NULL DEFAULT 0 AFTER organizer_id`); err != nil {
return err
}
if _, err := db.Exec(`UPDATE im_meetings SET recorder_user_id=organizer_id WHERE recorder_user_id=0`); err != nil {
return err
}
if _, err := db.Exec(`CREATE INDEX idx_im_meetings_recorder ON im_meetings(recorder_user_id,end_at)`); err != nil && !isDupe(err) {
return err
}
}
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS im_meeting_note_files(
id BIGINT NOT NULL AUTO_INCREMENT,
note_file_id VARCHAR(64) NOT NULL,
meeting_id VARCHAR(64) NOT NULL,
file_id VARCHAR(64) NOT NULL,
uploader_id BIGINT NOT NULL,
file_name VARCHAR(255) NOT NULL DEFAULT '',
mime_type VARCHAR(128) NOT NULL DEFAULT '',
file_size BIGINT NOT NULL DEFAULT 0,
file_etag VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'active',
preview_status VARCHAR(16) NOT NULL DEFAULT 'ready',
preview_mode VARCHAR(16) NOT NULL DEFAULT 'native',
preview_file_id VARCHAR(64) NOT NULL DEFAULT '',
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
deleted_by BIGINT,
deleted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_meeting_note_files_id(note_file_id),
INDEX idx_im_meeting_note_files_file(meeting_id,file_id,status),
UNIQUE INDEX uk_im_meeting_note_files_idempotency(meeting_id,uploader_id,idempotency_key),
INDEX idx_im_meeting_note_files_meeting(meeting_id,status,id DESC),
INDEX idx_im_meeting_note_files_uploader(uploader_id,id DESC)
)`); err != nil {
return err
}
_, err := db.Exec(`INSERT INTO im_workbench_apps(code,name,route,sort_order,unread_key,enabled,permission)
VALUES('meeting_notes','会议笔记','/meeting-notes',70,'',1,'enabled')
ON DUPLICATE KEY UPDATE name=VALUES(name),route=VALUES(route),sort_order=VALUES(sort_order),unread_key=VALUES(unread_key),updated_at=CURRENT_TIMESTAMP(3)`)
return err
}
func createIMReportTables(tx *sql.Tx) error {
if _, err := tx.Exec(
`CREATE TABLE im_daily_reports(
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
author_id BIGINT NOT NULL,
report_date DATE NOT NULL,
content TEXT NOT NULL,
attachments JSON,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
version INT NOT NULL DEFAULT 1,
submitted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_daily_reports_report(report_id),
UNIQUE INDEX uk_im_daily_reports_author_date(author_id, report_date),
INDEX idx_im_daily_reports_author_id(author_id, id DESC),
INDEX idx_im_daily_reports_date_status(report_date, status, id DESC)
)`); err != nil {
return err
}
_, err := tx.Exec(
`CREATE TABLE im_monthly_reports(
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
author_id BIGINT NOT NULL,
report_month CHAR(7) NOT NULL,
work_summary TEXT NOT NULL,
next_plan TEXT NOT NULL,
attachments JSON,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
version INT NOT NULL DEFAULT 1,
submitted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_monthly_reports_report(report_id),
UNIQUE INDEX uk_im_monthly_reports_author_month(author_id, report_month),
INDEX idx_im_monthly_reports_author_id(author_id, id DESC),
INDEX idx_im_monthly_reports_month_status(report_month, status, id DESC)
)`)
return err
}
func createIMMailboxTables(tx *sql.Tx) error {
if _, err := tx.Exec(
`CREATE TABLE im_mailbox_config(
id BIGINT NOT NULL AUTO_INCREMENT,
enabled TINYINT NOT NULL DEFAULT 1,
allow_anonymous TINYINT NOT NULL DEFAULT 1,
required_fields JSON,
max_length INT NOT NULL DEFAULT 5000,
attachment_policy JSON,
notice_title VARCHAR(128) NOT NULL DEFAULT '',
notice_content TEXT NOT NULL,
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id)
)`); err != nil {
return err
}
if _, err := tx.Exec(
`INSERT INTO im_mailbox_config(id,enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content)
VALUES(1,1,1,'["subject","content"]',5000,'{"max_files":5,"purpose":"executive_mailbox"}','总经理信箱','请如实填写诉求,匿名提交将按企业政策保护展示身份。')
ON DUPLICATE KEY UPDATE updated_at=updated_at`); err != nil {
return err
}
_, err := tx.Exec(
`CREATE TABLE im_mailbox_submissions(
id BIGINT NOT NULL AUTO_INCREMENT,
submission_id VARCHAR(64) NOT NULL,
sender_id BIGINT NOT NULL,
subject VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
anonymous TINYINT NOT NULL DEFAULT 0,
attachment_file_ids JSON,
notice_version VARCHAR(64) NOT NULL DEFAULT '',
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
submitted_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_mailbox_submissions_submission(submission_id),
UNIQUE INDEX uk_im_mailbox_submissions_idempotency(sender_id, idempotency_key),
INDEX idx_im_mailbox_submissions_sender(sender_id, id DESC),
INDEX idx_im_mailbox_submissions_status(status, id DESC)
)`)
return err
}
func createIMUserProductTables(tx *sql.Tx) error {
statements := []string{
`CREATE TABLE im_user_profiles(
@@ -4789,14 +4300,6 @@ func (a *adapter) FileDeleteUnused(olderThan time.Time, limit int) ([]string, er
var locations []string
var ids []any
for _, candidate := range candidates {
var noteRefs int
fileID := store.EncodeUid(int64(candidate.id)).String()
if err = tx.QueryRow(`SELECT COUNT(*) FROM im_meeting_note_files WHERE file_id=? AND status=? LIMIT 1`, fileID, t.MeetingNoteFileStatusActive).Scan(&noteRefs); err != nil {
break
}
if noteRefs > 0 {
continue
}
if candidate.location != "" {
locations = append(locations, candidate.location)
}
@@ -4914,23 +4417,13 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) {
defer cancel()
}
var allowed int
buildQuery := func(includeRecorder bool) (string, []any) {
recorderClause := ""
if includeRecorder {
recorderClause = `
OR mtg.recorder_user_id=?`
}
buildQuery := func() (string, []any) {
args := []any{
store.DecodeUid(fileID), fileID.String32(), "%/" + fileID.String32(), t.UploadCompleted,
store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), t.RelationshipFriend,
store.DecodeUid(uid), store.DecodeUid(uid), t.GroupMemberStatusActive, store.DecodeUid(uid), store.DecodeUid(uid),
store.DecodeUid(uid), fid, fid,
fid, t.MeetingStateCanceled, store.DecodeUid(uid),
}
if includeRecorder {
args = append(args, store.DecodeUid(uid))
}
args = append(args, store.DecodeUid(uid))
return `
SELECT COUNT(*) FROM im_fileuploads AS fu
WHERE (fu.id=? OR fu.location=? OR fu.location LIKE ?) AND fu.status=? AND (
@@ -4977,37 +4470,16 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) {
SELECT 1 FROM im_group_files AS gf
INNER JOIN im_subscriptions AS s
ON s.topic=gf.topic AND s.userid=?
WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%')) AND gf.status='active'
WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%')) AND gf.status='active'
AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0
)
OR EXISTS (
SELECT 1 FROM im_meeting_note_files AS mnf
INNER JOIN im_meetings AS mtg
ON mtg.meeting_id=mnf.meeting_id
WHERE mnf.file_id=? AND mnf.status='active'
AND mtg.state<>?
AND mtg.end_at<=CURRENT_TIMESTAMP(3)
AND (
mtg.organizer_id=?
` + recorderClause + `
OR EXISTS (
SELECT 1 FROM im_meeting_participants AS mp
WHERE mp.meeting_id=mtg.meeting_id AND mp.user_id=?
)
)
)
) LIMIT 1`, args
}
includeRecorder := true
run := func() error {
query, args := buildQuery(includeRecorder)
query, args := buildQuery()
return a.db.GetContext(ctx, &allowed, query, args...)
}
err := run()
if isUnknownColumn(err) && strings.Contains(strings.ToLower(err.Error()), "recorder_user_id") {
includeRecorder = false
err = run()
}
for attempt := 0; isRetryableMySQLReadErr(err) && attempt < 2; attempt++ {
time.Sleep(time.Duration(attempt+1) * 25 * time.Millisecond)
err = run()
@@ -86,8 +86,6 @@ func TestRuntimeOpenDoesNotAutoCreateOrMigrateTables(t *testing.T) {
"ensureIMAdminGovernanceTables",
"ensureIMFileReviewTables",
"ensureIMOrganizationVisibilityTables",
"ensureIMAttendanceTables",
"ensureIMMeetingNotesTables",
"ensureSnowflakePrimaryKeys",
"ensureUserIdentitySchema",
"ensureIMRegistrationInviteUseTable",
File diff suppressed because it is too large Load Diff
-34
View File
@@ -38,8 +38,6 @@ import (
// See https://www.iana.org/assignments/media-types/media-types.xhtml
var allowedMimeTypes = []string{"application/", "audio/", "font/", "image/", "text/", "video/"}
const fileUploadPurposeMeetingNote = "meeting_note"
// RequestPrincipal is a tenant-scoped identity authenticated for HTTP/gRPC file access.
type RequestPrincipal struct {
TenantID types.TenantID
@@ -257,12 +255,6 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
writeHttpResponse(ErrAuthRequired(msgID, "", now, now), nil)
return
}
meetingID, _, err := meetingNoteUploadParams(req, principal)
if err != nil {
writeHttpResponse(decodeStoreError(err, msgID, now, nil), err)
return
}
// Check if uploads are handled elsewhere.
headers, statusCode, err := mh.Headers(principal.TenantID, req.Method, req.URL, req.Header, true)
if err != nil {
@@ -374,36 +366,10 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
// How long this file is guaranteed to exist without being attached to a message or a topic.
params["expires"] = now.Add(globals.mediaGcPeriod).Format(types.TimeFormatRFC3339)
}
if meetingID != "" {
params["purpose"] = fileUploadPurposeMeetingNote
params["meeting_id"] = meetingID
}
writeHttpResponse(NoErrParams(msgID, "", now, params), nil)
logs.Info.Println("media upload: ok", fdef.Id, fdef.Location)
}
func meetingNoteUploadParams(req *http.Request, principal RequestPrincipal) (string, string, error) {
if strings.TrimSpace(req.FormValue("purpose")) != fileUploadPurposeMeetingNote {
return "", "", nil
}
if principal.UID.IsZero() {
return "", "", types.ErrPermissionDenied
}
meetingID := strings.TrimSpace(req.FormValue("meeting_id"))
if meetingID == "" {
return "", "", types.ErrMalformed
}
idempotencyKey := strings.TrimSpace(req.Header.Get("Idempotency-Key"))
if idempotencyKey == "" {
idempotencyKey = strings.TrimSpace(req.FormValue("id"))
}
if idempotencyKey == "" {
return "", "", types.ErrMalformed
}
return meetingID, idempotencyKey, nil
}
// LargeFileServe is the gRPC equivalent of largeFileServeHTTP.
func (*grpcNodeServer) LargeFileServe(req *pbx.FileDownReq, stream pbx.Node_LargeFileServeServer) error {
now := types.TimeNow()
-91
View File
@@ -1,11 +1,8 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
@@ -209,91 +206,3 @@ func TestLargeFileGarbageCollectTenantsScopesDeletes(t *testing.T) {
largeFileGarbageCollectTenants([]types.TenantID{7, 8}, olderThan, 25)
}
func TestLargeFileReceiveMeetingNoteUploadReturnsFileID(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldStore := store.Store
oldFiles := store.Files
oldMeetingNotes := store.MeetingNotes
oldKafkaBus := kafkaBus
defer func() {
store.Store = oldStore
store.Files = oldFiles
store.MeetingNotes = oldMeetingNotes
kafkaBus = oldKafkaBus
}()
handler := &testLargeFileMediaHandler{}
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
files := mock_store.NewMockFilePersistenceInterface(ctrl)
store.Store = mockStore
store.Files = files
store.MeetingNotes = nil
kafkaBus = nil
fileID := types.Uid(101)
mockStore.EXPECT().GetMediaHandler().Return(handler).AnyTimes()
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth)
mockStore.EXPECT().GetUidString().Return(fileID.String())
tokenAuth.EXPECT().Authenticate(auth.AuthContext{RemoteAddr: "192.0.2.1:12345"}, []byte("token-1")).
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil)
files.EXPECT().FinishUpload(types.TenantID(7), gomock.Any(), true, int64(11)).DoAndReturn(
func(_ types.TenantID, fd *types.FileDef, _ bool, size int64) (*types.FileDef, error) {
fd.Status = types.UploadCompleted
fd.Size = size
return fd, nil
})
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("id", "upload-meeting-note-1"); err != nil {
t.Fatal(err)
}
if err := writer.WriteField("purpose", fileUploadPurposeMeetingNote); err != nil {
t.Fatal(err)
}
if err := writer.WriteField("meeting_id", "mtg_test"); err != nil {
t.Fatal(err)
}
part, err := writer.CreateFormFile("file", "notes.txt")
if err != nil {
t.Fatal(err)
}
if _, err := part.Write([]byte("hello notes")); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/v0/file/u/", &body)
req.RemoteAddr = "192.0.2.1:12345"
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
req.Header.Set("X-MeetingIM-Auth", "token "+base64.StdEncoding.EncodeToString([]byte("token-1")))
rec := httptest.NewRecorder()
largeFileReceiveHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
var resp ServerComMessage
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v body=%s", err, rec.Body.String())
}
params := resp.Ctrl.Params.(map[string]any)
if params["file_id"] != fileID.String() || params["purpose"] != fileUploadPurposeMeetingNote ||
params["meeting_id"] != "mtg_test" {
t.Fatalf("unexpected upload params: %#v", params)
}
if _, ok := params["note_file_id"]; ok {
t.Fatalf("upload should not bind meeting note file: %#v", params)
}
if handler.data != "hello notes" || handler.def.OriginalName != "notes.txt" {
t.Fatalf("unexpected side effects: handler=%#v", handler)
}
}
-24
View File
@@ -74,30 +74,6 @@ func createIMSystemNotification(tenantID types.TenantID, uid types.Uid, eventID,
})
}
func publishIMMeetingEvent(tenantID types.TenantID, actor types.Uid, meeting *types.Meeting, eventType string) error {
if meeting == nil {
return types.ErrMalformed
}
for _, participant := range meeting.Participants {
uid := participant.UserID
if uid.IsZero() {
continue
}
eventID := stableIMEventID("meeting", meeting.MeetingID, meeting.Version, eventType, uid.UserId())
if uid != actor {
if err := createIMSystemNotification(tenantID, uid, stableIMEventID(eventID, "notification"), eventType,
"会议通知", meeting.Title, types.KVMap{"meeting_id": meeting.MeetingID}); err != nil {
return err
}
}
if err := enqueueIMBusinessEvent(tenantID, eventID, "user", uid.UserId(), actor, eventType,
map[string]any{"meeting_id": meeting.MeetingID, "version": meeting.Version, "state": meeting.State}); err != nil {
return err
}
}
return nil
}
func publishIMRelationEvent(tenantID types.TenantID, actor, target types.Uid, stableID, eventType, title, summary string) error {
if target.IsZero() {
return types.ErrMalformed
+4 -2312
View File
File diff suppressed because it is too large Load Diff
+5 -1423
View File
File diff suppressed because it is too large Load Diff
+2 -30
View File
@@ -96,12 +96,7 @@ var internalTenantResourceSpecs = map[string]*internalQuerySpec{
"feedback-config": {
Select: "SELECT id,categories,max_length,attachment_policy,allow_contact,diagnostics_notice,created_at,updated_at",
From: "FROM im_feedback_config", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
},
"mailbox-config": {
Select: "SELECT id,enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content,notice_updated_at,created_at,updated_at",
From: "FROM im_mailbox_config", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
},
"abuse-reasons": {
}, "abuse-reasons": {
Select: "SELECT id,target_type,reason_code,label,requires_description,allows_evidence,enabled,sort_order,created_at,updated_at",
From: "FROM im_abuse_reasons", DefaultSort: "ORDER BY sort_order ASC,id ASC", DetailKey: "id",
Filters: []internalQueryFilter{{"target_type", "target_type", "=", "string"}},
@@ -109,28 +104,10 @@ var internalTenantResourceSpecs = map[string]*internalQuerySpec{
"app-releases": {
Select: "SELECT id,platform,channel,latest_version,min_supported_version,update_type,release_notes,store_url,package_url,checksum,remind_after_seconds,published_at,created_at,updated_at",
From: "FROM im_app_releases", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
},
"workbench/apps": {
Select: "SELECT id,code AS app_code,name,icon_file_id,route,sort_order,unread_key,enabled,permission,created_at,updated_at",
From: "FROM im_workbench_apps", DefaultSort: "ORDER BY sort_order ASC,id ASC", DetailKey: "id",
},
}
}}
var internalOutboxResourceSpecs = map[string]*internalQuerySpec{}
var internalOrgResourceSpecs = map[string]*internalQuerySpec{
"departments": {
Select: "SELECT d.id,d.department_id,d.parent_id,d.name,d.sort_order,d.version,d.state,d.created_at,d.updated_at,(SELECT COUNT(*) FROM im_org_departments child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,(SELECT COUNT(*) FROM im_org_members m WHERE m.department_id=d.department_id) AS member_count",
From: "FROM im_org_departments d", DefaultSort: "ORDER BY d.sort_order ASC,d.id ASC",
Filters: []internalQueryFilter{{"department_id", "d.department_id", "=", "string"}, {"parent_id", "d.parent_id", "=", "string"}, {"name", "d.name", "LIKE", "string"}, {"state", "d.state", "=", "string"}},
},
"members": {
Select: "SELECT m.id,m.department_id,COALESCE(d.name,'') AS department_name,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at",
From: "FROM im_org_members m LEFT JOIN im_org_departments d ON d.department_id=m.department_id", DefaultSort: "ORDER BY m.sort_order ASC,m.id ASC",
Filters: []internalQueryFilter{{"department_id", "m.department_id", "=", "string"}, {"user_id", "m.user_id", "=", "uid"}, {"q", "m.display_name", "LIKE", "string"}, {"employment_state", "m.employment_state", "=", "string"}},
},
}
func serveInternalAdminResourceList(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, resource, scope string) {
spec, ok := internalAdminQuerySpec(resource)
if !ok {
@@ -255,10 +232,6 @@ func internalAdminQuerySpec(resource string) (*internalQuerySpec, bool) {
spec := internalOutboxResourceSpecs[strings.TrimPrefix(resource, "outbox_")]
return spec, spec != nil
}
if strings.HasPrefix(resource, "org_") {
spec := internalOrgResourceSpecs[strings.TrimPrefix(resource, "org_")]
return spec, spec != nil
}
spec := internalTenantResourceSpecs[resource]
return spec, spec != nil
}
@@ -275,7 +248,6 @@ func usersInternalQuerySpec() *internalQuerySpec {
CASE WHEN JSON_TYPE(JSON_EXTRACT(u.public,'$.photo'))='OBJECT' AND NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.photo.data')),'') IS NOT NULL
THEN CONCAT('data:image/',COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.photo.type')),''),'jpeg'),';base64,',JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.photo.data'))) ELSE NULL END,
NULLIF(CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.avatar_file_id')) REGEXP '^(https?://|data:)' THEN JSON_UNQUOTE(JSON_EXTRACT(u.public,'$.avatar_file_id')) ELSE '' END,''),
NULLIF((SELECT CASE WHEN om.avatar_file_id REGEXP '^(https?://|data:)' THEN om.avatar_file_id ELSE '' END FROM im_org_members AS om WHERE om.user_id=u.id AND om.avatar_file_id<>'' ORDER BY om.id LIMIT 1),''),
''
) AS avatar,
p.signature,sec.auth_version,sec.password_updated_at,u.createdat AS created_at,u.updatedat AS updated_at,
-4
View File
@@ -150,10 +150,6 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
serveInternalAdminResourceList(wrt, req, tenant.ID, "outbox_"+parts[3], "")
case len(parts) == 5 && parts[2] == "outbox" && internalOutboxResourceSpecs[parts[3]] != nil:
serveInternalAdminResourceDetail(wrt, req, tenant.ID, "outbox_"+parts[3], parts[4])
case len(parts) == 4 && parts[2] == "org" && internalOrgResourceSpecs[parts[3]] != nil:
serveInternalAdminResourceList(wrt, req, tenant.ID, "org_"+parts[3], "")
case len(parts) == 4 && parts[2] == "workbench" && parts[3] == "apps":
serveInternalAdminResourceList(wrt, req, tenant.ID, "workbench/apps", "")
case len(parts) == 4 && parts[2] == "dashboard" && parts[3] == "summary":
serveInternalDashboardSummary(wrt, req, tenant.ID)
case len(parts) == 4 && parts[2] == "kafka" && parts[3] == "dead-letters":
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"context"
"encoding/json"
"time"
"github.com/tinode/chat/server/store/types"
)
type kafkaAuditPayload struct {
ActorType string `json:"actor_type"`
ActorID string `json:"actor_id"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resource_id"`
Result string `json:"result"`
Reason string `json:"reason,omitempty"`
Data map[string]any `json:"data,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
}
var publishKafkaAuditEventEnvelope = publishKafkaEnvelope
func enqueueKafkaAuditEvent(ctx context.Context, tenantID types.TenantID, actorType string, actor types.Uid,
action, resource, resourceID, result, reason string, data map[string]any) error {
if !kafkaEnabled() {
return nil
}
actorID := ""
if !actor.IsZero() {
actorID = actor.UserId()
}
payload := kafkaAuditPayload{
ActorType: actorType,
ActorID: actorID,
Action: action,
Resource: resource,
ResourceID: resourceID,
Result: result,
Reason: reason,
Data: data,
OccurredAt: time.Now().UTC().Round(time.Millisecond),
}
encoded, err := json.Marshal(payload)
if err != nil {
return err
}
envelope := kafkaEnvelope{
EventID: stableIMEventID("audit_event", tenantID, "audit."+action, resource+":"+resourceID, encoded),
TenantID: tenantID,
SourceService: "im-server",
TargetService: "hotgo-admin",
EventType: "audit." + action,
BizType: "audit",
BizID: resource + ":" + resourceID,
OccurredAt: payload.OccurredAt,
Payload: encoded,
}
if !actor.IsZero() {
envelope.Actor = &kafkaActor{Type: actorType, ID: actorID}
}
return publishKafkaAuditEventEnvelope(ctx, "audit_event", envelope.BizID, envelope)
}
@@ -23,7 +23,7 @@ func TestKafkaTopicsKeepOnlyMessageInternalAsyncTopics(t *testing.T) {
if topics["audit_event"] != "im.audit.event" {
t.Fatalf("unexpected audit event topic: %q", topics["audit_event"])
}
for _, removed := range []string{"file_lifecycle", "workbench_event"} {
for _, removed := range []string{"file_lifecycle"} {
if _, ok := topics[removed]; ok {
t.Fatalf("internal sync topic %q must not be configured", removed)
}
@@ -116,30 +116,3 @@ func TestEnqueueKafkaFileAttachmentsBindRunsSynchronously(t *testing.T) {
t.Fatalf("enqueueKafkaFileAttachmentsBind returned error: %v", err)
}
}
func TestEnqueueKafkaWorkbenchEventRunsSynchronously(t *testing.T) {
oldKafkaBus := kafkaBus
oldInfrastructure := store.HTTPInfrastructure
oldSecurity := store.Security
defer func() {
kafkaBus = oldKafkaBus
store.HTTPInfrastructure = oldInfrastructure
store.Security = oldSecurity
}()
kafkaBus = &kafkaRuntime{}
security := &testSecurityStore{}
store.Security = security
err := enqueueKafkaWorkbenchEvent(context.Background(), 7, types.Uid(42), "meeting", "meeting.created", "meeting-1",
[]types.Uid{42, 43}, "会议通知", "周会", map[string]any{"meeting_id": "meeting-1"})
if err != nil {
t.Fatalf("enqueueKafkaWorkbenchEvent returned error: %v", err)
}
if len(security.notifications) != 1 {
t.Fatalf("expected one notification for non-actor target, got %#v", security.notifications)
}
if security.notifications[0].UserID != types.Uid(43) || security.notifications[0].EventType != "meeting.created" {
t.Fatalf("unexpected notification: %#v", security.notifications[0])
}
}
-2
View File
@@ -24,7 +24,6 @@ const (
kafkaTopicMessagePersist = "im.message.persist"
kafkaTopicGroupMessagePersist = "im.group.message.persist"
kafkaTopicMessagePush = "im.message.push"
kafkaTopicWorkbenchExternal = "im.workbench.event"
kafkaTopicFileSecurity = "im.file.security"
kafkaTopicCacheInvalidate = "im.cache.invalidate"
kafkaTopicAuditEvent = "im.audit.event"
@@ -211,7 +210,6 @@ func kafkaTopics(overrides map[string]string) map[string]string {
"message_persist": kafkaTopicMessagePersist,
"group_message_persist": kafkaTopicGroupMessagePersist,
"message_push": kafkaTopicMessagePush,
"workbench_external": kafkaTopicWorkbenchExternal,
"file_security": kafkaTopicFileSecurity,
"cache_invalidate": kafkaTopicCacheInvalidate,
"audit_event": kafkaTopicAuditEvent,
-129
View File
@@ -1,129 +0,0 @@
package main
import (
"context"
"encoding/json"
"time"
"github.com/tinode/chat/server/store/types"
)
type kafkaWorkbenchPayload struct {
Module string `json:"module"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
ActorUserID string `json:"actor_user_id"`
TargetUserIDs []string `json:"target_user_ids,omitempty"`
Title string `json:"title,omitempty"`
Summary string `json:"summary,omitempty"`
Data map[string]any `json:"data,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
}
type kafkaAuditPayload struct {
ActorType string `json:"actor_type"`
ActorID string `json:"actor_id"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resource_id"`
Result string `json:"result"`
Reason string `json:"reason,omitempty"`
Data map[string]any `json:"data,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
}
var publishKafkaAuditEventEnvelope = publishKafkaEnvelope
func enqueueKafkaWorkbenchEvent(ctx context.Context, tenantID types.TenantID, actor types.Uid, module, action, resourceID string,
targets []types.Uid, title, summary string, data map[string]any) error {
targetUserIDs := make([]string, 0, len(targets))
for _, target := range targets {
if !target.IsZero() {
targetUserIDs = append(targetUserIDs, target.UserId())
}
}
payload := kafkaWorkbenchPayload{
Module: module,
Action: action,
ResourceID: resourceID,
ActorUserID: actor.UserId(),
TargetUserIDs: targetUserIDs,
Title: title,
Summary: summary,
Data: data,
OccurredAt: time.Now().UTC().Round(time.Millisecond),
}
envelope := kafkaEnvelope{
EventID: stableIMEventID("workbench-sync", tenantID, "workbench."+action, resourceID, payload.OccurredAt),
TenantID: tenantID,
EventType: "workbench." + action,
BizType: "workbench",
BizID: resourceID,
}
return executeKafkaWorkbenchEvent(tenantID, envelope, payload)
}
func enqueueKafkaAuditEvent(ctx context.Context, tenantID types.TenantID, actorType string, actor types.Uid,
action, resource, resourceID, result, reason string, data map[string]any) error {
if !kafkaEnabled() {
return nil
}
actorID := ""
if !actor.IsZero() {
actorID = actor.UserId()
}
payload := kafkaAuditPayload{
ActorType: actorType,
ActorID: actorID,
Action: action,
Resource: resource,
ResourceID: resourceID,
Result: result,
Reason: reason,
Data: data,
OccurredAt: time.Now().UTC().Round(time.Millisecond),
}
encoded, err := json.Marshal(payload)
if err != nil {
return err
}
envelope := kafkaEnvelope{
EventID: stableIMEventID("audit_event", tenantID, "audit."+action, resource+":"+resourceID, encoded),
TenantID: tenantID,
SourceService: "im-server",
TargetService: "hotgo-admin",
EventType: "audit." + action,
BizType: "audit",
BizID: resource + ":" + resourceID,
OccurredAt: payload.OccurredAt,
Payload: encoded,
}
if !actor.IsZero() {
envelope.Actor = &kafkaActor{Type: actorType, ID: actorID}
}
return publishKafkaAuditEventEnvelope(ctx, "audit_event", envelope.BizID, envelope)
}
func executeKafkaWorkbenchEvent(tenantID types.TenantID, envelope kafkaEnvelope, payload kafkaWorkbenchPayload) error {
actor := types.ParseUserId(payload.ActorUserID)
if actor.IsZero() {
actor = types.ParseUid(payload.ActorUserID)
}
for _, raw := range payload.TargetUserIDs {
target := types.ParseUserId(raw)
if target.IsZero() {
continue
}
eventID := stableIMEventID("workbench", envelope.EventID, target.UserId())
if target != actor && payload.Title != "" {
if err := createIMSystemNotification(tenantID, target, stableIMEventID(eventID, "notification"), payload.Action,
payload.Title, payload.Summary, types.KVMap{"module": payload.Module, "resource_id": payload.ResourceID}); err != nil {
return err
}
}
if err := enqueueIMBusinessEvent(tenantID, eventID, "user", target.UserId(), actor, payload.Action, payload.Data); err != nil {
return err
}
}
return nil
}
-163
View File
@@ -1,163 +0,0 @@
package main
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/segmentio/kafka-go"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
type kafkaExternalWorkbenchEvent struct {
EventID string `json:"event_id"`
TraceID string `json:"trace_id,omitempty"`
TenantID types.TenantID `json:"tenant_id,omitempty"`
TenantCode string `json:"tenant_code,omitempty"`
ActorUserID int64 `json:"actor_user_id"`
Module string `json:"module"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
TargetUserIDs []int64 `json:"target_user_ids,omitempty"`
Title string `json:"title,omitempty"`
Summary string `json:"summary,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
Data map[string]any `json:"data,omitempty"`
}
func startKafkaWorkbenchEventConsumer() {
startKafkaConsumer("workbench_external", handleKafkaWorkbenchEvent)
}
func handleKafkaWorkbenchEvent(ctx context.Context, envelope kafkaEnvelope, msg kafka.Message) error {
payload, normalized, err := decodeKafkaWorkbenchEvent(envelope, msg)
if err != nil {
logs.Warn.Printf("kafka workbench event malformed: topic=%s offset=%d err=%v", msg.Topic, msg.Offset, err)
return err
}
tenantID, err := kafkaTenantIDFromEnvelope(normalized)
if err != nil {
return err
}
consume, claimed, err := beginKafkaConsumeForTopic(tenantID, normalized, "workbench_external")
if err != nil {
return err
}
if !claimed {
logs.Info.Printf("kafka workbench event skipped: event=%s group=%s status=%s", normalized.EventID, kafkaBus.config.ConsumerGroup, consume.Status)
return nil
}
if err = executeKafkaExternalWorkbenchEvent(tenantID, normalized, payload); err != nil {
reason := kafkaCommandErrorReason(err)
failed, markErr := store.HTTPInfrastructure.MarkKafkaConsumeFailed(tenantID, normalized.EventID, kafkaBus.config.ConsumerGroup, reason, err.Error(), time.Now().UTC().Round(time.Millisecond))
if markErr != nil {
return markErr
}
if failed != nil && failed.RetryCount >= kafkaBus.config.MaxRetryCount {
return markKafkaDeadLetter(ctx, tenantID, normalized, failed, reason, err.Error())
}
if failed != nil {
sleepKafkaRetryBackoff(failed.RetryCount)
}
return err
}
return store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(tenantID, normalized.EventID, kafkaBus.config.ConsumerGroup, time.Now().UTC().Round(time.Millisecond))
}
func decodeKafkaWorkbenchEvent(envelope kafkaEnvelope, msg kafka.Message) (kafkaExternalWorkbenchEvent, kafkaEnvelope, error) {
var payload kafkaExternalWorkbenchEvent
if len(envelope.Payload) > 0 {
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
return payload, envelope, err
}
} else if err := json.Unmarshal(msg.Value, &payload); err != nil {
return payload, envelope, err
}
if strings.TrimSpace(payload.EventID) == "" {
payload.EventID = envelope.EventID
}
if strings.TrimSpace(payload.Action) == "" && envelope.EventType != "" {
payload.Action = strings.TrimPrefix(envelope.EventType, "workbench.")
}
if strings.TrimSpace(payload.EventID) == "" || strings.TrimSpace(payload.Action) == "" || strings.TrimSpace(payload.ResourceID) == "" {
return payload, envelope, types.ErrMalformed
}
if payload.OccurredAt.IsZero() {
payload.OccurredAt = time.Now().UTC().Round(time.Millisecond)
}
normalized := kafkaEnvelope{
EventID: payload.EventID,
TraceID: firstNonEmpty(payload.TraceID, envelope.TraceID),
TenantCode: firstNonEmpty(payload.TenantCode, envelope.TenantCode),
TenantID: firstTenantID(payload.TenantID, envelope.TenantID),
SourceService: firstNonEmpty(envelope.SourceService, "im-workbench"),
TargetService: firstNonEmpty(envelope.TargetService, "im-server"),
EventType: "workbench." + payload.Action,
BizType: firstNonEmpty(payload.Module, envelope.BizType, "workbench"),
BizID: firstNonEmpty(payload.ResourceID, envelope.BizID),
OccurredAt: payload.OccurredAt,
Payload: msg.Value,
}
if payload.ActorUserID > 0 {
normalized.Actor = &kafkaActor{Type: "user", ID: types.Uid(payload.ActorUserID).UserId()}
}
return payload, normalized, nil
}
func executeKafkaExternalWorkbenchEvent(tenantID types.TenantID, envelope kafkaEnvelope, payload kafkaExternalWorkbenchEvent) error {
if store.Security == nil {
return types.ErrUnsupported
}
actor := types.Uid(payload.ActorUserID)
data := types.KVMap{
"module": payload.Module,
"resource_id": payload.ResourceID,
}
for k, v := range payload.Data {
data[k] = v
}
targets := compactWorkbenchTargets(payload.TargetUserIDs, actor)
for _, target := range targets {
targetID := target.UserId()
eventID := stableIMEventID("workbench", envelope.EventID, targetID)
if target != actor && payload.Title != "" {
if err := createIMSystemNotification(tenantID, target, stableIMEventID(eventID, "notification"), payload.Action,
payload.Title, payload.Summary, data); err != nil {
return err
}
}
if err := enqueueIMBusinessEvent(tenantID, eventID, "user", targetID, actor, payload.Action, data); err != nil {
return err
}
}
return nil
}
func compactWorkbenchTargets(raw []int64, actor types.Uid) []types.Uid {
seen := make(map[types.Uid]bool, len(raw)+1)
targets := make([]types.Uid, 0, len(raw)+1)
for _, value := range raw {
uid := types.Uid(value)
if uid.IsZero() || seen[uid] {
continue
}
seen[uid] = true
targets = append(targets, uid)
}
if len(targets) == 0 && !actor.IsZero() {
targets = append(targets, actor)
}
return targets
}
func firstTenantID(values ...types.TenantID) types.TenantID {
for _, value := range values {
if !value.IsZero() {
return value
}
}
return types.ZeroTenantID
}
-89
View File
@@ -1,89 +0,0 @@
package main
import (
"testing"
"time"
"github.com/segmentio/kafka-go"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
func TestKafkaTopicsIncludeExternalWorkbenchEvent(t *testing.T) {
topics := kafkaTopics(nil)
if topics["workbench_external"] != "im.workbench.event" {
t.Fatalf("unexpected workbench external topic: %q", topics["workbench_external"])
}
if _, ok := topics["workbench_event"]; ok {
t.Fatal("legacy in-process workbench_event topic key must not be configured")
}
}
func TestDecodeKafkaWorkbenchEventDirectPayload(t *testing.T) {
msg := kafka.Message{Value: []byte(`{
"event_id":"wbe_1",
"trace_id":"trace-1",
"tenant_id":7,
"tenant_code":"default",
"actor_user_id":42,
"module":"meeting",
"action":"meeting.created",
"resource_id":"meeting-1",
"target_user_ids":[42,43],
"title":"会议通知",
"summary":"周会",
"occurred_at":"2026-09-13T10:00:00Z",
"data":{"meeting_id":"meeting-1"}
}`)}
payload, envelope, err := decodeKafkaWorkbenchEvent(kafkaEnvelope{}, msg)
if err != nil {
t.Fatalf("decodeKafkaWorkbenchEvent returned error: %v", err)
}
if payload.EventID != "wbe_1" || payload.ActorUserID != 42 || len(payload.TargetUserIDs) != 2 {
t.Fatalf("unexpected payload: %#v", payload)
}
if envelope.EventID != "wbe_1" || envelope.EventType != "workbench.meeting.created" ||
envelope.BizType != "meeting" || envelope.BizID != "meeting-1" {
t.Fatalf("unexpected normalized envelope: %#v", envelope)
}
if envelope.Actor == nil || envelope.Actor.ID != types.Uid(42).UserId() {
t.Fatalf("unexpected actor: %#v", envelope.Actor)
}
if envelope.OccurredAt.IsZero() || envelope.TenantID != types.TenantID(7) {
t.Fatalf("unexpected occurrence or tenant: %#v", envelope)
}
}
func TestExecuteKafkaExternalWorkbenchEventCreatesNotificationForNonActor(t *testing.T) {
oldSecurity := store.Security
defer func() { store.Security = oldSecurity }()
security := &testSecurityStore{}
store.Security = security
payload := kafkaExternalWorkbenchEvent{
EventID: "wbe_1",
TenantID: 7,
ActorUserID: 42,
Module: "meeting",
Action: "meeting.created",
ResourceID: "meeting-1",
TargetUserIDs: []int64{42, 43},
Title: "会议通知",
Summary: "周会",
OccurredAt: time.Date(2026, 9, 13, 10, 0, 0, 0, time.UTC),
Data: map[string]any{"meeting_id": "meeting-1"},
}
envelope := kafkaEnvelope{EventID: payload.EventID}
if err := executeKafkaExternalWorkbenchEvent(7, envelope, payload); err != nil {
t.Fatalf("executeKafkaExternalWorkbenchEvent returned error: %v", err)
}
if len(security.notifications) != 1 {
t.Fatalf("expected one notification for non-actor target, got %#v", security.notifications)
}
if security.notifications[0].UserID != types.Uid(43) || security.notifications[0].EventType != "meeting.created" {
t.Fatalf("unexpected notification: %#v", security.notifications[0])
}
}
-22
View File
@@ -758,7 +758,6 @@ func main() {
startKafkaConfigEventConsumer()
startKafkaCacheInvalidateConsumer()
startKafkaMessagePersistConsumer()
startKafkaWorkbenchEventConsumer()
startKafkaDeadLetterConsumer()
// Start accepting cluster traffic.
@@ -886,29 +885,8 @@ func main() {
mux.HandleFunc(config.ApiPath+"v1/im/security/realname/appeals", serveIMRealNameAppeals)
mux.HandleFunc(config.ApiPath+"v1/im/security/realname/submissions", serveIMRealNameSubmissions)
mux.HandleFunc(config.ApiPath+"v1/im/bootstrap", serveIMBootstrap)
mux.HandleFunc(config.ApiPath+"v1/im/workbench", serveIMWorkbench)
mux.HandleFunc(config.ApiPath+"v1/im/organization/departments", serveIMOrganizationDepartments)
mux.HandleFunc(config.ApiPath+"v1/im/organization/departments/", serveIMOrganizationDepartmentResource)
mux.HandleFunc(config.ApiPath+"v1/im/organization/members", serveIMOrganizationMembers)
mux.HandleFunc(config.ApiPath+"v1/im/organization/search", serveIMOrganizationSearch)
mux.HandleFunc(config.ApiPath+"v1/im/attendance/config", serveIMAttendanceConfig)
mux.HandleFunc(config.ApiPath+"v1/im/attendance/records", serveIMAttendanceRecords)
mux.HandleFunc(config.ApiPath+"v1/im/attendance/checkins", serveIMAttendanceCheckins)
mux.HandleFunc(config.ApiPath+"v1/im/attendance/appeals", serveIMAttendanceAppeals)
mux.HandleFunc(config.ApiPath+"v1/im/meeting-notes", serveIMMeetingNotes)
mux.HandleFunc(config.ApiPath+"v1/im/meetings", serveIMMeetings)
mux.HandleFunc(config.ApiPath+"v1/im/meetings/", serveIMMeetingResource)
mux.HandleFunc(config.ApiPath+"v1/im/reports/abuse/reasons", serveIMAbuseReasons)
mux.HandleFunc(config.ApiPath+"v1/im/reports/abuse", serveIMAbuseReports)
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily/statistics", serveIMDailyReportStatistics)
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily/reminders", serveIMDailyReportReminders)
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily", serveIMDailyReports)
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily/", serveIMDailyReportResource)
mux.HandleFunc(config.ApiPath+"v1/im/reports/monthly/statistics", serveIMMonthlyReportStatistics)
mux.HandleFunc(config.ApiPath+"v1/im/reports/monthly", serveIMMonthlyReports)
mux.HandleFunc(config.ApiPath+"v1/im/reports/monthly/", serveIMMonthlyReportResource)
mux.HandleFunc(config.ApiPath+"v1/im/executive-mailbox/config", serveIMMailboxConfig)
mux.HandleFunc(config.ApiPath+"v1/im/executive-mailbox/submissions", serveIMMailboxSubmissions)
mux.HandleFunc(config.ApiPath+"v1/im/feedback/config", serveIMFeedbackConfig)
mux.HandleFunc(config.ApiPath+"v1/im/feedback", serveIMFeedback)
mux.HandleFunc(config.ApiPath+"v1/im/app/releases/latest", serveIMAppReleaseLatest)
-1
View File
@@ -137,7 +137,6 @@
"message_persist": "im.message.persist",
"group_message_persist": "im.group.message.persist",
"message_push": "im.message.push",
"workbench_external": "im.workbench.event",
"file_security": "im.file.security",
"cache_invalidate": "im.cache.invalidate",
"audit_event": "im.audit.event",
-603
View File
@@ -2307,601 +2307,6 @@ func groupMemberCacheID(topic string, uid types.Uid) string {
return strings.TrimSpace(topic) + ":" + uid.String()
}
type WorkbenchPersistenceInterface interface {
Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error)
}
type workbenchMapper struct{}
var Workbench WorkbenchPersistenceInterface
func (workbenchMapper) Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error) {
if uid.IsZero() {
return nil, 0, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, 0, err
}
return tenantAdp.TenantWorkbenchModules(tenantID, uid)
}
type OrganizationPersistenceInterface interface {
ListDepartments(tenantID types.TenantID, uid types.Uid, parentID, cursor string, limit int) ([]types.OrganizationDepartment, string, error)
ListMembers(tenantID types.TenantID, uid types.Uid, departmentID, cursor string, limit int) ([]types.OrganizationMember, string, error)
ListAllMembers(tenantID types.TenantID, uid types.Uid, departmentID, queryText, cursor string, limit int) ([]types.OrganizationMember, string, error)
Search(tenantID types.TenantID, uid types.Uid, queryText, resultTypes, cursor string, limit int) ([]types.OrganizationSearchItem, string, error)
}
type organizationMapper struct{}
var Organization OrganizationPersistenceInterface
func (organizationMapper) ListDepartments(tenantID types.TenantID, uid types.Uid, parentID, cursor string, limit int) ([]types.OrganizationDepartment, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantOrganizationDepartments(tenantID, uid, strings.TrimSpace(parentID), strings.TrimSpace(cursor), limit)
}
func (organizationMapper) ListMembers(tenantID types.TenantID, uid types.Uid, departmentID, cursor string, limit int) ([]types.OrganizationMember, string, error) {
if uid.IsZero() || strings.TrimSpace(departmentID) == "" {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantOrganizationMembers(tenantID, uid, strings.TrimSpace(departmentID), strings.TrimSpace(cursor), limit)
}
func (organizationMapper) ListAllMembers(tenantID types.TenantID, uid types.Uid, departmentID, queryText, cursor string, limit int) ([]types.OrganizationMember, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantOrganizationMembersAll(tenantID, uid, strings.TrimSpace(departmentID), strings.TrimSpace(queryText), strings.TrimSpace(cursor), limit)
}
func (organizationMapper) Search(tenantID types.TenantID, uid types.Uid, queryText, resultTypes, cursor string, limit int) ([]types.OrganizationSearchItem, string, error) {
if uid.IsZero() || strings.TrimSpace(queryText) == "" {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantOrganizationSearch(tenantID, uid, strings.TrimSpace(queryText), strings.TrimSpace(resultTypes), strings.TrimSpace(cursor), limit)
}
type AttendancePersistenceInterface interface {
Config(tenantID types.TenantID, uid types.Uid, date time.Time) (*types.AttendanceConfig, error)
ListRecords(tenantID types.TenantID, uid types.Uid, opts types.AttendanceRecordListOptions) ([]types.AttendanceRecord, string, *types.AttendanceRecordStats, error)
Checkin(tenantID types.TenantID, uid types.Uid, checkinID string, req types.AttendanceCheckinRequest) (*types.AttendanceRecord, *types.AttendanceCheckin, error)
CreateAppeal(tenantID types.TenantID, uid types.Uid, appeal *types.AttendanceAppeal, idempotencyKey string) (*types.AttendanceAppeal, error)
ListAppeals(tenantID types.TenantID, uid types.Uid, opts types.AttendanceAppealListOptions) ([]types.AttendanceAppeal, string, error)
}
type attendanceMapper struct{}
var Attendance AttendancePersistenceInterface
func (attendanceMapper) Config(tenantID types.TenantID, uid types.Uid, date time.Time) (*types.AttendanceConfig, error) {
if uid.IsZero() || date.IsZero() {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantAttendanceConfig(tenantID, uid, date)
}
func (attendanceMapper) ListRecords(tenantID types.TenantID, uid types.Uid, opts types.AttendanceRecordListOptions) ([]types.AttendanceRecord, string, *types.AttendanceRecordStats, error) {
if uid.IsZero() {
return nil, "", nil, types.ErrMalformed
}
if opts.Limit <= 0 || opts.Limit > 100 {
opts.Limit = 50
}
opts.Scope = strings.TrimSpace(opts.Scope)
if opts.Scope == "" {
opts.Scope = "mine"
}
if opts.Scope != "mine" && opts.Scope != "managed" {
return nil, "", nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", nil, err
}
return tenantAdp.TenantAttendanceRecordList(tenantID, uid, opts)
}
func (attendanceMapper) Checkin(tenantID types.TenantID, uid types.Uid, checkinID string, req types.AttendanceCheckinRequest) (*types.AttendanceRecord, *types.AttendanceCheckin, error) {
if uid.IsZero() || strings.TrimSpace(checkinID) == "" || req.Date.IsZero() || req.ClientTime.IsZero() || !validAttendanceAction(req.Action) {
return nil, nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, nil, err
}
return tenantAdp.TenantAttendanceCheckin(tenantID, uid, strings.TrimSpace(checkinID), req)
}
func (attendanceMapper) CreateAppeal(tenantID types.TenantID, uid types.Uid, appeal *types.AttendanceAppeal, idempotencyKey string) (*types.AttendanceAppeal, error) {
if uid.IsZero() || appeal == nil || strings.TrimSpace(appeal.AppealID) == "" || strings.TrimSpace(appeal.RecordID) == "" || strings.TrimSpace(appeal.Reason) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantAttendanceAppealCreate(tenantID, uid, appeal, strings.TrimSpace(idempotencyKey))
}
func (attendanceMapper) ListAppeals(tenantID types.TenantID, uid types.Uid, opts types.AttendanceAppealListOptions) ([]types.AttendanceAppeal, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if opts.Limit <= 0 || opts.Limit > 100 {
opts.Limit = 50
}
opts.Scope = strings.TrimSpace(opts.Scope)
if opts.Scope == "" {
opts.Scope = "mine"
}
if opts.Scope != "mine" && opts.Scope != "managed" {
return nil, "", types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantAttendanceAppealList(tenantID, uid, opts)
}
func validAttendanceAction(action string) bool {
switch strings.TrimSpace(action) {
case types.AttendanceActionCheckIn, types.AttendanceActionCheckOut:
return true
default:
return false
}
}
type MeetingsPersistenceInterface interface {
Create(tenantID types.TenantID, uid types.Uid, meeting *types.Meeting, attendeeIDs []types.Uid) (*types.Meeting, error)
List(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.Meeting, string, error)
Get(tenantID types.TenantID, uid types.Uid, meetingID string) (*types.Meeting, error)
Update(tenantID types.TenantID, uid types.Uid, meetingID string, patch types.MeetingPatch, idempotencyKey string) (*types.Meeting, error)
Cancel(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) (*types.Meeting, error)
Remind(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) ([]types.MeetingParticipant, error)
Respond(tenantID types.TenantID, uid types.Uid, meetingID, response string, version int) (*types.Meeting, error)
}
type MeetingNotesPersistenceInterface interface {
List(tenantID types.TenantID, uid types.Uid, query, cursor string, limit int) ([]types.Meeting, string, error)
ListFiles(tenantID types.TenantID, uid types.Uid, meetingID string) ([]types.MeetingNoteFile, error)
CreateFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string) (*types.MeetingNoteFile, error)
ReplaceFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error)
DeleteFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error)
}
type meetingsMapper struct{}
type meetingNotesMapper struct{}
var Meetings MeetingsPersistenceInterface
var MeetingNotes MeetingNotesPersistenceInterface
func (meetingsMapper) Create(tenantID types.TenantID, uid types.Uid, meeting *types.Meeting, attendeeIDs []types.Uid) (*types.Meeting, error) {
if uid.IsZero() || meeting == nil || !validMeetingForCreate(meeting) {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingCreate(tenantID, uid, meeting, attendeeIDs)
}
func (meetingsMapper) List(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.Meeting, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 20
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantMeetingList(tenantID, uid, strings.TrimSpace(cursor), limit)
}
func (meetingsMapper) Get(tenantID types.TenantID, uid types.Uid, meetingID string) (*types.Meeting, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingGet(tenantID, uid, strings.TrimSpace(meetingID))
}
func (meetingsMapper) Update(tenantID types.TenantID, uid types.Uid, meetingID string, patch types.MeetingPatch, idempotencyKey string) (*types.Meeting, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || patch.Version <= 0 || !validMeetingPatch(patch) {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingUpdate(tenantID, uid, strings.TrimSpace(meetingID), patch, strings.TrimSpace(idempotencyKey))
}
func (meetingsMapper) Cancel(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) (*types.Meeting, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingCancel(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(idempotencyKey))
}
func (meetingsMapper) Remind(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) ([]types.MeetingParticipant, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingRemind(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(idempotencyKey))
}
func (meetingsMapper) Respond(tenantID types.TenantID, uid types.Uid, meetingID, response string, version int) (*types.Meeting, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || !validMeetingResponse(response) || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingRespond(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(response), version)
}
func (meetingNotesMapper) List(tenantID types.TenantID, uid types.Uid, query, cursor string, limit int) ([]types.Meeting, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 20
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantMeetingNoteList(tenantID, uid, strings.TrimSpace(query), strings.TrimSpace(cursor), limit)
}
func (meetingNotesMapper) ListFiles(tenantID types.TenantID, uid types.Uid, meetingID string) ([]types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFiles(tenantID, uid, strings.TrimSpace(meetingID))
}
func (meetingNotesMapper) CreateFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string) (*types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || strings.TrimSpace(noteFileID) == "" || strings.TrimSpace(fileID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFileCreate(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(noteFileID), strings.TrimSpace(fileID), strings.TrimSpace(idempotencyKey))
}
func (meetingNotesMapper) ReplaceFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || strings.TrimSpace(noteFileID) == "" || strings.TrimSpace(fileID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFileReplace(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(noteFileID), strings.TrimSpace(fileID), strings.TrimSpace(idempotencyKey), version)
}
func (meetingNotesMapper) DeleteFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || strings.TrimSpace(noteFileID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFileDelete(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(noteFileID), strings.TrimSpace(idempotencyKey), version)
}
func validMeetingForCreate(meeting *types.Meeting) bool {
return strings.TrimSpace(meeting.MeetingID) != "" &&
strings.TrimSpace(meeting.Title) != "" &&
!meeting.StartAt.IsZero() &&
!meeting.EndAt.IsZero() &&
meeting.EndAt.After(meeting.StartAt)
}
func validMeetingPatch(patch types.MeetingPatch) bool {
if patch.Title == nil && patch.RecorderID == nil && patch.StartAt == nil && patch.EndAt == nil && patch.Location == nil && patch.ReminderMinutes == nil {
return false
}
if patch.Title != nil && strings.TrimSpace(*patch.Title) == "" {
return false
}
if patch.StartAt != nil && patch.EndAt != nil && !patch.EndAt.After(*patch.StartAt) {
return false
}
return true
}
func validMeetingResponse(response string) bool {
switch strings.TrimSpace(response) {
case types.MeetingResponseAccepted, types.MeetingResponseDeclined, types.MeetingResponseTentative:
return true
default:
return false
}
}
type DailyReportsPersistenceInterface interface {
List(tenantID types.TenantID, uid types.Uid, scope, from, to, status, cursor string, limit int) ([]types.DailyReport, string, error)
CreateDraft(tenantID types.TenantID, uid types.Uid, reportID string, reportDate time.Time) (*types.DailyReport, error)
Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.DailyReport, error)
SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, content string, attachments []string, version int) (*types.DailyReport, error)
Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.DailyReport, error)
Stats(tenantID types.TenantID, uid types.Uid, date time.Time, departmentID string) (*types.DailyReportStats, error)
Remind(tenantID types.TenantID, uid types.Uid, date time.Time, userIDs []types.Uid, idempotencyKey string) ([]types.Uid, error)
}
type dailyReportsMapper struct{}
var DailyReports DailyReportsPersistenceInterface
func (dailyReportsMapper) List(tenantID types.TenantID, uid types.Uid, scope, from, to, status, cursor string, limit int) ([]types.DailyReport, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantDailyReportList(tenantID, uid, strings.TrimSpace(scope), strings.TrimSpace(from), strings.TrimSpace(to), strings.TrimSpace(status), strings.TrimSpace(cursor), limit)
}
func (dailyReportsMapper) CreateDraft(tenantID types.TenantID, uid types.Uid, reportID string, reportDate time.Time) (*types.DailyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" || reportDate.IsZero() {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantDailyReportCreateDraft(tenantID, uid, strings.TrimSpace(reportID), reportDate)
}
func (dailyReportsMapper) Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.DailyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantDailyReportGet(tenantID, uid, strings.TrimSpace(reportID))
}
func (dailyReportsMapper) SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, content string, attachments []string, version int) (*types.DailyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantDailyReportSaveDraft(tenantID, uid, strings.TrimSpace(reportID), content, attachments, version)
}
func (dailyReportsMapper) Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.DailyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantDailyReportSubmit(tenantID, uid, strings.TrimSpace(reportID), version)
}
func (dailyReportsMapper) Stats(tenantID types.TenantID, uid types.Uid, date time.Time, departmentID string) (*types.DailyReportStats, error) {
if uid.IsZero() || date.IsZero() {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantDailyReportStats(tenantID, uid, date, strings.TrimSpace(departmentID))
}
func (dailyReportsMapper) Remind(tenantID types.TenantID, uid types.Uid, date time.Time, userIDs []types.Uid, idempotencyKey string) ([]types.Uid, error) {
if uid.IsZero() || date.IsZero() || strings.TrimSpace(idempotencyKey) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantDailyReportRemind(tenantID, uid, date, userIDs, strings.TrimSpace(idempotencyKey))
}
type MonthlyReportsPersistenceInterface interface {
List(tenantID types.TenantID, uid types.Uid, scope, fromMonth, toMonth, status, cursor string, limit int) ([]types.MonthlyReport, string, error)
CreateDraft(tenantID types.TenantID, uid types.Uid, reportID, month string) (*types.MonthlyReport, error)
Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.MonthlyReport, error)
SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, workSummary, nextPlan string, attachments []string, version int) (*types.MonthlyReport, error)
Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error)
Reopen(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error)
Stats(tenantID types.TenantID, uid types.Uid, month, departmentID string) (*types.MonthlyReportStats, error)
}
type monthlyReportsMapper struct{}
var MonthlyReports MonthlyReportsPersistenceInterface
func (monthlyReportsMapper) List(tenantID types.TenantID, uid types.Uid, scope, fromMonth, toMonth, status, cursor string, limit int) ([]types.MonthlyReport, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantMonthlyReportList(tenantID, uid, strings.TrimSpace(scope), strings.TrimSpace(fromMonth), strings.TrimSpace(toMonth), strings.TrimSpace(status), strings.TrimSpace(cursor), limit)
}
func (monthlyReportsMapper) CreateDraft(tenantID types.TenantID, uid types.Uid, reportID, month string) (*types.MonthlyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" || strings.TrimSpace(month) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMonthlyReportCreateDraft(tenantID, uid, strings.TrimSpace(reportID), strings.TrimSpace(month))
}
func (monthlyReportsMapper) Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.MonthlyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMonthlyReportGet(tenantID, uid, strings.TrimSpace(reportID))
}
func (monthlyReportsMapper) SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, workSummary, nextPlan string, attachments []string, version int) (*types.MonthlyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMonthlyReportSaveDraft(tenantID, uid, strings.TrimSpace(reportID), workSummary, nextPlan, attachments, version)
}
func (monthlyReportsMapper) Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMonthlyReportSubmit(tenantID, uid, strings.TrimSpace(reportID), version)
}
func (monthlyReportsMapper) Reopen(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error) {
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMonthlyReportReopen(tenantID, uid, strings.TrimSpace(reportID), version)
}
func (monthlyReportsMapper) Stats(tenantID types.TenantID, uid types.Uid, month, departmentID string) (*types.MonthlyReportStats, error) {
if uid.IsZero() || strings.TrimSpace(month) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMonthlyReportStats(tenantID, uid, strings.TrimSpace(month), strings.TrimSpace(departmentID))
}
type MailboxPersistenceInterface interface {
Config(tenantID types.TenantID, uid types.Uid) (*types.MailboxConfig, error)
Submit(tenantID types.TenantID, uid types.Uid, submission *types.MailboxSubmission, idempotencyKey string) (*types.MailboxSubmission, error)
}
type mailboxMapper struct{}
var Mailbox MailboxPersistenceInterface
func (mailboxMapper) Config(tenantID types.TenantID, uid types.Uid) (*types.MailboxConfig, error) {
if uid.IsZero() {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMailboxConfigGet(tenantID, uid)
}
func (mailboxMapper) Submit(tenantID types.TenantID, uid types.Uid, submission *types.MailboxSubmission, idempotencyKey string) (*types.MailboxSubmission, error) {
if uid.IsZero() || submission == nil || strings.TrimSpace(submission.SubmissionID) == "" ||
strings.TrimSpace(submission.Subject) == "" || strings.TrimSpace(submission.Content) == "" ||
strings.TrimSpace(idempotencyKey) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMailboxSubmissionCreate(tenantID, uid, submission, strings.TrimSpace(idempotencyKey))
}
type ProfilePersistenceInterface interface {
Get(tenantID types.TenantID, uid types.Uid) (*types.UserProfile, error)
Update(tenantID types.TenantID, uid types.Uid, patch types.UserProfilePatch) (*types.UserProfile, error)
@@ -5175,14 +4580,6 @@ func init() {
Conversations = conversationsMapper{}
Calls = callsMapper{}
GroupBusiness = groupBusinessMapper{}
Workbench = workbenchMapper{}
Organization = organizationMapper{}
Attendance = attendanceMapper{}
Meetings = meetingsMapper{}
MeetingNotes = meetingNotesMapper{}
DailyReports = dailyReportsMapper{}
MonthlyReports = monthlyReportsMapper{}
Mailbox = mailboxMapper{}
Profiles = profileMapper{}
AppReleases = appReleaseMapper{}
DevicePushTokens = devicePushTokenMapper{}
-153
View File
@@ -1,153 +0,0 @@
package types
import "time"
const (
AttendanceActionCheckIn = "check_in"
AttendanceActionCheckOut = "check_out"
AttendanceStatusNotChecked = "not_checked"
AttendanceStatusCheckedIn = "checked_in"
AttendanceStatusNormal = "normal"
AttendanceStatusLate = "late"
AttendanceStatusEarlyLeave = "early_leave"
AttendanceStatusAbnormal = "abnormal"
AttendanceCheckinNormal = "normal"
AttendanceCheckinLate = "late"
AttendanceCheckinEarlyLeave = "early_leave"
AttendanceCheckinOutsideRange = "outside_range"
AttendanceAppealPending = "pending"
AttendanceAppealApproved = "approved"
AttendanceAppealRejected = "rejected"
AttendanceAppealCanceled = "canceled"
)
type AttendanceRule struct {
ID int64
RuleID string
Name string
Workday bool
LocationRequired bool
WifiRequired bool
AllowOutsideCheckin bool
AppealAllowed bool
AppealDeadline *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type AttendanceShift struct {
ID int64
ShiftID string
RuleID string
Name string
WorkStart string
WorkEnd string
CheckinStart string
CheckoutEnd string
SortOrder int
CreatedAt time.Time
UpdatedAt time.Time
}
type AttendanceCheckin struct {
ID int64
CheckinID string
RecordID string
UserID Uid
Date time.Time
Action string
RuleID string
ShiftID string
ClientTime time.Time
ServerTime time.Time
Result string
Location KVMap
Wifi KVMap
Device KVMap
IdempotencyKey string
CreatedAt time.Time
}
type AttendanceRecord struct {
ID int64
RecordID string
UserID Uid
Date time.Time
RuleID string
ShiftID string
Status string
Checkins []AttendanceCheckin
Exceptions []string
AppealStatus string
Version int
CreatedAt time.Time
UpdatedAt time.Time
}
type AttendanceConfig struct {
Date time.Time
Timezone string
Rule AttendanceRule
Shifts []AttendanceShift
AllowedActions []string
TodayRecord *AttendanceRecord
CanViewManaged bool
}
type AttendanceRecordListOptions struct {
Scope string
From string
To string
Date string
Status string
DepartmentID string
UserID string
Cursor string
Limit int
}
type AttendanceRecordStats struct {
ExpectedCount int
CheckedCount int
ExceptionCount int
NotCheckedCount int
}
type AttendanceCheckinRequest struct {
Date time.Time
Action string
RuleID string
ShiftID string
ClientTime time.Time
Location KVMap
Wifi KVMap
Device KVMap
IdempotencyKey string
}
type AttendanceAppeal struct {
ID int64
AppealID string
RecordID string
UserID Uid
AppealType string
Reason string
TargetAction string
TargetTime *time.Time
AttachmentFileIDs []string
Status string
Version int
SubmittedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type AttendanceAppealListOptions struct {
Scope string
Status string
Cursor string
Limit int
}
-36
View File
@@ -1,36 +0,0 @@
package types
import "time"
const MailboxSubmissionStatusReceived = "received"
type MailboxNotice struct {
Title string
Content string
UpdatedAt time.Time
}
type MailboxConfig struct {
Enabled bool
AllowAnonymous bool
RequiredFields []string
MaxLength int
AttachmentPolicy map[string]any
Notice MailboxNotice
UpdatedAt time.Time
}
type MailboxSubmission struct {
ID int64
SubmissionID string
SenderID Uid
Subject string
Content string
Anonymous bool
AttachmentFileIDs []string
NoticeVersion string
Status string
SubmittedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
-83
View File
@@ -1,83 +0,0 @@
package types
import "time"
const (
MeetingStateScheduled = "scheduled"
MeetingStateEnded = "ended"
MeetingStateCanceled = "canceled"
MeetingResponsePending = "pending"
MeetingResponseAccepted = "accepted"
MeetingResponseDeclined = "declined"
MeetingResponseTentative = "tentative"
)
type Meeting struct {
ID int64
MeetingID string
OrganizerID Uid
RecorderID Uid
Title string
StartAt time.Time
EndAt time.Time
Location string
State string
ReminderMinutes []int
IdempotencyKey string
Version int
CanceledAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
Participants []MeetingParticipant
}
type MeetingParticipant struct {
ID int64
MeetingID string
UserID Uid
Response string
ResponseAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type MeetingPatch struct {
Title *string
RecorderID *Uid
StartAt *time.Time
EndAt *time.Time
Location *string
ReminderMinutes []int
Version int
}
const (
MeetingNoteStatusEmpty = "empty"
MeetingNoteStatusAvailable = "available"
MeetingNoteFileStatusActive = "active"
MeetingNoteFileStatusDeleted = "deleted"
)
type MeetingNoteFile struct {
ID int64
NoteFileID string
MeetingID string
FileID string
UploaderID Uid
FileName string
MIMEType string
FileSize int64
FileETag string
Status string
PreviewStatus string
PreviewMode string
PreviewFileID string
IdempotencyKey string
Version int
DeletedBy Uid
DeletedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
-40
View File
@@ -1,40 +0,0 @@
package types
import "time"
type OrganizationDepartment struct {
ID int64 `db:"id"`
DepartmentID string `db:"department_id"`
Name string `db:"name"`
ParentID string `db:"parent_id"`
SortOrder int `db:"sort_order"`
ChildCount int `db:"child_count"`
MemberCount int `db:"member_count"`
Version int `db:"version"`
State string `db:"state"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type OrganizationMember struct {
ID int64 `db:"id"`
DepartmentID string `db:"department_id"`
UserID Uid `db:"-"`
DisplayName string `db:"display_name"`
Position string `db:"position"`
AvatarFileID string `db:"avatar_file_id"`
Mobile string `db:"mobile"`
Email string `db:"email"`
Status string `db:"employment_state"`
Contactable bool `db:"contactable"`
SortOrder int `db:"sort_order"`
Version int `db:"version"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type OrganizationSearchItem struct {
Type string
Department *OrganizationDepartment
Member *OrganizationMember
}
-51
View File
@@ -1,51 +0,0 @@
package types
import "time"
const (
DailyReportStatusDraft = "draft"
DailyReportStatusSubmitted = "submitted"
)
type DailyReport struct {
ID int64
ReportID string
AuthorID Uid
ReportDate time.Time
Content string
Attachments []string
Status string
ReadState string
Version int
SubmittedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type DailyReportStats struct {
ExpectedCount int
SubmittedCount int
MissingCount int
}
type MonthlyReport struct {
ID int64
ReportID string
AuthorID Uid
Month string
WorkSummary string
NextPlan string
Attachments []string
Status string
ReadState string
Version int
SubmittedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type MonthlyReportStats struct {
ExpectedCount int
SubmittedCount int
MissingCount int
}
-18
View File
@@ -1,18 +0,0 @@
package types
import "time"
type WorkbenchModule struct {
ID int64 `db:"id"`
Code string `db:"code"`
Name string `db:"name"`
IconFileID string `db:"icon_file_id"`
Route string `db:"route"`
SortOrder int `db:"sort_order"`
UnreadKey string `db:"unread_key"`
UnreadCount int `db:"unread_count"`
Enabled bool `db:"enabled"`
Permission string `db:"permission"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
-1
View File
@@ -137,7 +137,6 @@
"message_persist": "im.message.persist",
"group_message_persist": "im.group.message.persist",
"message_push": "im.message.push",
"workbench_external": "im.workbench.event",
"file_security": "im.file.security",
"cache_invalidate": "im.cache.invalidate",
"audit_event": "im.audit.event",