更新角色管理相关内容

This commit is contained in:
2026-09-09 15:04:34 +08:00
parent de7225b61e
commit fef0233cc4
24 changed files with 1442 additions and 116 deletions
+188 -16
View File
@@ -2,6 +2,7 @@ package controllers
import (
"encoding/json"
"fmt"
"io"
"regexp"
"strconv"
@@ -289,14 +290,103 @@ func (c *BackendOaScheduleController) Stats() {
}
type oaSchedulePayload struct {
Title string `json:"title"`
Content string `json:"content"`
ScheduleDate string `json:"schedule_date"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
AllDay int8 `json:"all_day"`
Color string `json:"color"`
Priority int8 `json:"priority"`
Title string `json:"title"`
Content string `json:"content"`
ScheduleDate string `json:"schedule_date"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
AllDay int8 `json:"all_day"`
Color string `json:"color"`
Priority int8 `json:"priority"`
// 提醒绑定:开启后会在提醒工具生成一条提醒任务,并把其 ID 回写到 ReminderID。
ReminderEnabled bool `json:"reminder_enabled"`
ReminderTime string `json:"reminder_time"`
RemindChannels []string `json:"remind_channels"`
AdvanceMinutes int `json:"advance_minutes"`
}
// oaReminderChannels 允许的提醒渠道集合。
var oaReminderChannels = map[string]bool{
"SMS": true, "EMAIL": true, "BARK": true, "SITE_MSG": true,
}
// createOaScheduleReminder 为 OA 日程创建一条绑定的提醒任务。
// 复用提醒工具的数据模型(yz_backend_schedule + yz_backend_schedule_reminder):
// 生成一条 BackendSchedule,再按渠道逐条生成提醒记录,返回 BackendSchedule.ID
// 作为 OA 日程的 reminder_id。EMAIL/BARK 渠道会附带确认回执令牌与重复发送配置。
func createOaScheduleReminder(tid int, userID uint64, title, content, reminderTime string, channels []string, advanceMinutes int) (uint64, error) {
if strings.TrimSpace(reminderTime) == "" {
return 0, fmt.Errorf("提醒时间不能为空")
}
schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", reminderTime, time.Local)
if err != nil {
return 0, fmt.Errorf("提醒时间格式不合法")
}
if advanceMinutes < 0 {
advanceMinutes = 0
}
title = strings.TrimSpace(title)
if title == "" {
title = "日程提醒"
}
bs := models.BackendSchedule{
Tid: tid,
Title: title,
Content: content,
ScheduleTime: schedTime,
UserID: userID,
}
schedID, err := models.Orm.Insert(&bs)
if err != nil {
return 0, fmt.Errorf("创建提醒任务失败: %v", err)
}
now := time.Now()
for _, ch := range channels {
ch = strings.ToUpper(strings.TrimSpace(ch))
if !oaReminderChannels[ch] {
continue
}
firstSend := schedTime.Add(-time.Duration(advanceMinutes) * time.Minute)
reminder := models.BackendScheduleReminder{
Tid: tid,
ScheduleID: uint64(schedID),
RemindChannel: ch,
AdvanceMinutes: advanceMinutes,
NextRemindTime: firstSend,
ReceiverUserID: userID,
RemindStatus: 0,
CreateTime: now,
UpdateTime: now,
}
if ch == "EMAIL" || ch == "BARK" {
token := backendGenerateToken()
reminder.AckToken = &token
reminder.RepeatIntervalMinutes = 10
reminder.MaxSendCount = 5
} else {
reminder.RepeatIntervalMinutes = 0
reminder.MaxSendCount = 1
}
if _, err := models.Orm.Insert(&reminder); err != nil {
return 0, fmt.Errorf("创建提醒失败: %v", err)
}
}
return uint64(schedID), nil
}
// deleteBoundReminder 软删除 OA 日程绑定的提醒任务及其提醒记录。
// 删除日程或关闭提醒时使用,避免提醒工具里残留孤立任务。
func deleteBoundReminder(scheduleID uint64) {
if scheduleID == 0 {
return
}
_, _ = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", scheduleID).Delete()
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", scheduleID).
Update(map[string]interface{}{"IsDeleted": 1, "UpdateTime": time.Now()})
}
// oaScheduleImage 日程相关图片(截图/照片),只存文件引用,不存二进制。
@@ -393,6 +483,29 @@ func (c *BackendOaScheduleController) parseOaSchedulePayload() (oaSchedulePayloa
if payload.Color == "" {
payload.Color = "#409EFF"
}
// 提醒绑定校验:开启提醒时必须选择渠道且提醒时间合法。
if payload.ReminderEnabled {
validChannels := make([]string, 0, len(payload.RemindChannels))
for _, ch := range payload.RemindChannels {
ch = strings.ToUpper(strings.TrimSpace(ch))
if oaReminderChannels[ch] {
validChannels = append(validChannels, ch)
}
}
payload.RemindChannels = validChannels
if len(validChannels) == 0 {
c.oasJsonErr(400, 400, "请至少选择一个提醒渠道")
return payload, false
}
if _, err := time.ParseInLocation("2006-01-02 15:04:05", payload.ReminderTime, time.Local); err != nil {
c.oasJsonErr(400, 400, "提醒时间格式不合法,支持 YYYY-MM-DD HH:mm:ss")
return payload, false
}
if payload.AdvanceMinutes < 0 {
payload.AdvanceMinutes = 0
}
}
return payload, true
}
@@ -432,6 +545,30 @@ func (c *BackendOaScheduleController) Create() {
return
}
// 开启提醒:生成绑定的提醒任务,并把其 ID 回写到日程。
if payload.ReminderEnabled {
rid, rerr := createOaScheduleReminder(
claims.TenantId,
uint64(claims.UserID),
payload.Title,
payload.Content,
payload.ReminderTime,
payload.RemindChannels,
payload.AdvanceMinutes,
)
if rerr != nil {
// 回滚已创建的日程,避免产生孤立日程
_, _ = models.Orm.QueryTable(new(models.OaSchedule)).Filter("id", item.ID).Delete()
c.oasJsonErr(500, 500, rerr.Error())
return
}
item.ReminderID = rid
if _, err := models.Orm.Update(item, "ReminderID"); err != nil {
c.oasJsonErr(500, 500, "保存失败")
return
}
}
c.oasOk(item)
}
@@ -477,9 +614,34 @@ func (c *BackendOaScheduleController) Update() {
item.Priority = payload.Priority
item.UpdateTime = &now
// 重新绑定提醒:先创建新提醒,成功后再删除旧提醒(关闭提醒则清掉绑定)。
// 顺序反过来可避免"新提醒创建失败却删掉了原提醒"导致提醒丢失。
if payload.ReminderEnabled {
rid, rerr := createOaScheduleReminder(
claims.TenantId,
uint64(claims.UserID),
payload.Title,
payload.Content,
payload.ReminderTime,
payload.RemindChannels,
payload.AdvanceMinutes,
)
if rerr != nil {
c.oasJsonErr(500, 500, rerr.Error())
return
}
if item.ReminderID > 0 {
deleteBoundReminder(item.ReminderID)
}
item.ReminderID = rid
} else if item.ReminderID > 0 {
deleteBoundReminder(item.ReminderID)
item.ReminderID = 0
}
if _, err := models.Orm.Update(&item,
"Title", "Content", "ScheduleDate", "StartTime", "EndTime",
"AllDay", "Color", "Priority", "UpdateTime"); err != nil {
"AllDay", "Color", "Priority", "ReminderID", "UpdateTime"); err != nil {
c.oasJsonErr(500, 500, "保存失败")
return
}
@@ -555,21 +717,31 @@ func (c *BackendOaScheduleController) Delete() {
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.OaSchedule)).
var item models.OaSchedule
err = models.Orm.QueryTable(new(models.OaSchedule)).
Filter("id", id).
Filter("is_deleted", 0).
Filter("tid", claims.TenantId).
Filter("user_id", claims.UserID).
Update(map[string]interface{}{
"IsDeleted": int8(1),
"DeleteTime": now,
"UpdateTime": now,
})
if err != nil || n == 0 {
One(&item)
if err != nil {
c.oasJsonErr(404, 404, "日程不存在或已删除")
return
}
// 先删除绑定的提醒任务,避免提醒工具里残留孤立任务
if item.ReminderID > 0 {
deleteBoundReminder(item.ReminderID)
}
item.IsDeleted = 1
item.DeleteTime = &now
item.UpdateTime = &now
if _, err := models.Orm.Update(&item, "IsDeleted", "DeleteTime", "UpdateTime"); err != nil {
c.oasJsonErr(500, 500, "删除失败")
return
}
c.oasOk(nil)
}
+2
View File
@@ -138,9 +138,11 @@ func (c *BackendRoleController) CreateRole() {
status = *p.Status
}
rights := normalizeRights(p.Rights)
// 租户端创建的角色一律标记为“自定义角色”,与系统/平台预置角色区分,并按租户隔离
role := &models.AdminRole{
TenantID: tid,
Cid: 2,
IsCustom: 1,
Name: p.Name,
Status: status,
Rights: rights,
+1
View File
@@ -7,6 +7,7 @@ type AdminRole struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantID uint64 `orm:"column(tenant_id);default(0)" json:"tenant_id"` // 租户ID:0=平台/全局,>0=具体租户(仅 cid=2 租户角色使用)
Cid uint8 `orm:"column(cid);default(1)" json:"cid"` // 1平台角色 2租户角色
IsCustom uint8 `orm:"column(is_custom);default(0)" json:"is_custom"` // 是否租户自定义角色:0=系统/平台预置 1=租户在租户端自建(数据按租户隔离)
Name string `orm:"column(name);size(32)" json:"name"`
Status uint8 `orm:"column(status);default(1)" json:"status"`
Rights *string `orm:"column(rights);type(text);null" json:"rights"`
+17 -1
View File
@@ -7,6 +7,7 @@ import (
)
var adminRoleTenantColOnce sync.Once
var adminRoleCustomColOnce sync.Once
// EnsureAdminRoleTenantColumn 为角色表补齐 tenant_id 列(历史库可能早于租户隔离特性建表)。
// 字段已存在时 MySQL 报 duplicate column,统一忽略,不阻断主流程。
@@ -22,6 +23,20 @@ func EnsureAdminRoleTenantColumn() {
})
}
// EnsureAdminRoleCustomColumn 为角色表补齐 is_custom 列(历史库可能早于自定义角色特性建表)。
// 字段已存在时 MySQL 报 duplicate column,统一忽略,不阻断主流程。
func EnsureAdminRoleCustomColumn() {
if Orm == nil {
return
}
adminRoleCustomColOnce.Do(func() {
_, _ = Orm.Raw(`
ALTER TABLE yz_system_admin_role
ADD COLUMN is_custom tinyint unsigned NOT NULL DEFAULT 0 COMMENT '自定义角色 0-系统/平台预置 1-租户自建'
`).Exec()
})
}
// EnsureDefaultTenantRoles 保证指定租户(cid=2)的默认角色齐全:
// - 租户管理员:全权限(rights 为空,见 permission.go 空权限即放行)
// - 部门负责人
@@ -33,8 +48,9 @@ func EnsureDefaultTenantRoles(tid uint64) {
if Orm == nil || tid == 0 {
return
}
// 确保 tenant_id 列存在(兼容老表)
// 确保 tenant_id / is_custom 列存在(兼容老表)
EnsureAdminRoleTenantColumn()
EnsureAdminRoleCustomColumn()
defaults := []string{"租户管理员", "部门负责人", "员工"}
for _, name := range defaults {
+4
View File
@@ -123,4 +123,8 @@ func Init(_ string) {
// 创建全局 Ormer
Orm = orm.NewOrm()
// 补齐历史库缺失的列(已存在时 MySQL 报 duplicate column,统一忽略)
EnsureAdminRoleTenantColumn()
EnsureAdminRoleCustomColumn()
}
+4
View File
@@ -35,6 +35,9 @@ type OaSchedule struct {
CarryOver int8 `orm:"column(carry_over);default(0)" json:"carry_over"`
CarryCount int `orm:"column(carry_count);default(0)" json:"carry_count"`
CarryToDate string `orm:"column(carry_to_date);size(10);default()" json:"carry_to_date"`
// ReminderID 绑定到提醒工具的提醒任务(yz_backend_schedule.id),0 表示未开启提醒。
// 用于把 OA 日程与提醒任务关联,从而在日程详情回显提醒状态(如"提醒已关闭")。
ReminderID uint64 `orm:"column(reminder_id);default(0)" json:"reminder_id"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
@@ -62,6 +65,7 @@ func EnsureOaScheduleCarryColumns() error {
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN carry_count int NOT NULL DEFAULT 0 COMMENT '累计顺延次数'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN carry_to_date varchar(10) NOT NULL DEFAULT '' COMMENT '顺延目标日期(YYYY-MM-DD)'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN images text NULL COMMENT '相关图片JSON数组 [{id,url,name}]'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN reminder_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '绑定的提醒任务ID(对应yz_backend_schedule.id) 0-无提醒'",
}
for _, sql := range alters {
if _, e := Orm.Raw(sql).Exec(); e != nil {