package store import ( "context" "database/sql" "strings" "time" ) type AdminWorkbenchModule 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"` Enabled int `db:"enabled"` Permission string `db:"permission"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } type AdminWorkbenchModuleInput struct { ID int64 Code string Name string IconFileID string Route string SortOrder int UnreadKey string Enabled int Permission string } type AdminWorkbenchReorderItem struct { Code string SortOrder int } type AdminMailboxConfig struct { ID int64 `db:"id"` Enabled int `db:"enabled"` AllowAnonymous int `db:"allow_anonymous"` RequiredFields string `db:"required_fields"` MaxLength int `db:"max_length"` AttachmentPolicy string `db:"attachment_policy"` NoticeTitle string `db:"notice_title"` NoticeContent string `db:"notice_content"` NoticeUpdatedAt time.Time `db:"notice_updated_at"` UpdatedAt time.Time `db:"updated_at"` } type AdminFeedbackConfig struct { ID int64 `db:"id"` Categories string `db:"categories"` MaxLength int `db:"max_length"` AttachmentPolicy string `db:"attachment_policy"` AllowContact int `db:"allow_contact"` DiagnosticsNotice string `db:"diagnostics_notice"` UpdatedAt time.Time `db:"updated_at"` } func (s *Store) AdminWorkbenchModules(ctx context.Context, page, perPage int, code, name string, enabled *int) ([]AdminWorkbenchModule, int, error) { if page <= 0 { page = 1 } if perPage <= 0 { perPage = 20 } if perPage > 200 { perPage = 200 } where := []string{"1=1"} args := []any{} if code = strings.TrimSpace(code); code != "" { where = append(where, "code LIKE ?") args = append(args, "%"+code+"%") } if name = strings.TrimSpace(name); name != "" { where = append(where, "name LIKE ?") args = append(args, "%"+name+"%") } if enabled != nil { where = append(where, "enabled=?") args = append(args, *enabled) } whereSQL := strings.Join(where, " AND ") var total int if err := s.db.GetContext(ctx, &total, "SELECT COUNT(*) FROM im_workbench_apps WHERE "+whereSQL, args...); err != nil { return nil, 0, err } queryArgs := append([]any{}, args...) queryArgs = append(queryArgs, (page-1)*perPage, perPage) var items []AdminWorkbenchModule err := s.db.SelectContext(ctx, &items, `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 `+whereSQL+` ORDER BY sort_order ASC,id ASC LIMIT ?,?`, queryArgs...) return items, total, err } func (s *Store) UpsertAdminWorkbenchModule(ctx context.Context, in AdminWorkbenchModuleInput) (*AdminWorkbenchModule, error) { now := time.Now().UTC().Round(time.Millisecond) in.Code = strings.TrimSpace(in.Code) in.Name = strings.TrimSpace(in.Name) in.IconFileID = strings.TrimSpace(in.IconFileID) in.Route = strings.TrimSpace(in.Route) in.UnreadKey = strings.TrimSpace(in.UnreadKey) in.Permission = strings.TrimSpace(in.Permission) if in.Permission == "" { 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) if err != nil { return nil, err } if rows, _ := res.RowsAffected(); rows == 0 { return nil, ErrNotFound } 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) if err != nil { return nil, err } id, _ := res.LastInsertId() return s.adminWorkbenchModuleByID(ctx, id) } func (s *Store) DisableAdminWorkbenchModule(ctx context.Context, id int64, code string) error { now := time.Now().UTC().Round(time.Millisecond) 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) } else { res, err = s.db.ExecContext(ctx, `UPDATE im_workbench_apps SET enabled=0,updated_at=? WHERE code=?`, now, strings.TrimSpace(code)) } if err != nil { return err } if rows, _ := res.RowsAffected(); rows == 0 { return ErrNotFound } return nil } func (s *Store) ReorderAdminWorkbenchModules(ctx context.Context, items []AdminWorkbenchReorderItem) error { tx, err := s.db.BeginTxx(ctx, nil) if err != nil { return err } 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 { return err } } return tx.Commit() } 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 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 } return nil, err } return &cfg, nil } 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,?,?,?,?,?,?,?,?,?,?) 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) 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 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 } return nil, err } return &cfg, nil } 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,?,?,?,?,?,?,?) 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) 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 errorsIsNoRows(err) { return nil, ErrNotFound } return nil, err } return &item, nil } func errorsIsNoRows(err error) bool { return err == sql.ErrNoRows } func defaultString(value string, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value }