优化日报模块
This commit is contained in:
1 parent
79d5ae6b66
commit
51fd90fc51
17 files changed
+1767
-257
No files matched your search
@@ -22,21 +22,21 @@ type Attachment struct {
|
||||
|
||||
// ReportItem 日报列表/详情中的主体。
|
||||
type ReportItem struct {
|
||||
ID uint64 `json:"id"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
OrgID uint64 `json:"org_id"`
|
||||
OrgName string `json:"org_name"`
|
||||
ReportDate string `json:"report_date"`
|
||||
TodayDone string `json:"today_done"`
|
||||
TomorrowPlan string `json:"tomorrow_plan"`
|
||||
Issues string `json:"issues"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
Status int8 `json:"status"`
|
||||
SubmitTime string `json:"submit_time"`
|
||||
CreateTime string `json:"create_time"`
|
||||
UpdateTime string `json:"update_time"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
ID uint64 `json:"id"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
OrgID uint64 `json:"org_id"`
|
||||
OrgName string `json:"org_name"`
|
||||
ReportDate string `json:"report_date"`
|
||||
TodayDone string `json:"today_done"`
|
||||
TomorrowPlan string `json:"tomorrow_plan"`
|
||||
Issues string `json:"issues"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
Status int8 `json:"status"`
|
||||
SubmitTime string `json:"submit_time"`
|
||||
CreateTime string `json:"create_time"`
|
||||
UpdateTime string `json:"update_time"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
}
|
||||
|
||||
// Comment 日报评论(两级回复)。
|
||||
@@ -52,13 +52,29 @@ type Comment struct {
|
||||
CanDelete bool `json:"can_delete"`
|
||||
}
|
||||
|
||||
// Detail 日报详情:主体 + 评论 + 当前用户可执行的操作。
|
||||
// Detail 日报详情:主体 + 评论 + 修改痕迹 + 当前用户可执行的操作。
|
||||
type Detail struct {
|
||||
Report ReportItem `json:"report"`
|
||||
Comments []Comment `json:"comments"`
|
||||
CanEdit bool `json:"can_edit"`
|
||||
CanDelete bool `json:"can_delete"`
|
||||
CanComment bool `json:"can_comment"`
|
||||
Report ReportItem `json:"report"`
|
||||
Comments []Comment `json:"comments"`
|
||||
Histories []HistoryItem `json:"histories"`
|
||||
IsAuthor bool `json:"is_author"`
|
||||
CanEdit bool `json:"can_edit"`
|
||||
CanDelete bool `json:"can_delete"`
|
||||
CanComment bool `json:"can_comment"`
|
||||
CanWithdraw bool `json:"can_withdraw"`
|
||||
}
|
||||
|
||||
// HistoryItem 日报修改痕迹:撤销提交 / 撤销后重新提交时的内容快照。
|
||||
type HistoryItem struct {
|
||||
ID uint64 `json:"id"`
|
||||
Action int8 `json:"action"` // 1撤销提交 2重新提交
|
||||
UserName string `json:"user_name"`
|
||||
TodayDone string `json:"today_done"`
|
||||
TomorrowPlan string `json:"tomorrow_plan"`
|
||||
Issues string `json:"issues"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
SubmitTime string `json:"submit_time"`
|
||||
CreateTime string `json:"create_time"`
|
||||
}
|
||||
|
||||
// SaveInput 保存日报入参;Submit=true 表示提交,否则保存草稿。
|
||||
@@ -97,6 +113,7 @@ type MineResult struct {
|
||||
}
|
||||
|
||||
// Save 新建或更新一条日报(同一人同一天唯一)。
|
||||
// 已提交的日报处于锁定状态,必须先撤销提交退回草稿才能修改。
|
||||
func Save(u *UserContext, in SaveInput) (*ReportItem, error) {
|
||||
date, err := ParseDate(in.Date)
|
||||
if err != nil {
|
||||
@@ -166,11 +183,18 @@ func Save(u *UserContext, in SaveInput) (*ReportItem, error) {
|
||||
return nil, findErr
|
||||
}
|
||||
|
||||
// 已提交的日报在窗口内允许继续修改,状态保持已提交。
|
||||
// 已提交的日报锁定:必须先撤销提交退回草稿,才能修改内容。
|
||||
if existing.Status == 1 {
|
||||
status = 1
|
||||
return nil, errBad("日报已提交并锁定,如需修改请先撤销提交")
|
||||
}
|
||||
if status == 1 && existing.SubmitTime == nil {
|
||||
// 撤销过提交的日报再次提交时,留下重新提交痕迹。
|
||||
resubmit := false
|
||||
if in.Submit {
|
||||
if existing.SubmitTime != nil {
|
||||
resubmit = true
|
||||
} else {
|
||||
resubmit = hasWithdrawHistory(u.Tid, existing.ID)
|
||||
}
|
||||
existing.SubmitTime = &now
|
||||
}
|
||||
existing.TodayDone = in.TodayDone
|
||||
@@ -182,8 +206,35 @@ func Save(u *UserContext, in SaveInput) (*ReportItem, error) {
|
||||
existing.OrgID = u.OrgID
|
||||
existing.OrgName = u.OrgName
|
||||
|
||||
if _, err := models.Orm.Update(&existing, "today_done", "tomorrow_plan", "issues",
|
||||
tx, err := models.Orm.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.Update(&existing, "today_done", "tomorrow_plan", "issues",
|
||||
"attachments", "status", "submit_time", "user_name", "org_id", "org_name"); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if resubmit {
|
||||
history := models.OaDailyReportHistory{
|
||||
Tid: u.Tid,
|
||||
ReportID: existing.ID,
|
||||
UserID: u.UID,
|
||||
UserName: u.Name,
|
||||
ReportDate: date,
|
||||
Action: 2,
|
||||
TodayDone: existing.TodayDone,
|
||||
TomorrowPlan: existing.TomorrowPlan,
|
||||
Issues: existing.Issues,
|
||||
Attachments: existing.Attachments,
|
||||
SubmitTime: existing.SubmitTime,
|
||||
}
|
||||
if _, err := tx.Insert(&history); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := commentCount(u.Tid, existing.ID)
|
||||
@@ -245,6 +296,67 @@ func ListMine(u *UserContext, q MineQuery) (*MineResult, error) {
|
||||
return &MineResult{List: list, Total: total, Summary: *summary}, nil
|
||||
}
|
||||
|
||||
// CalendarDay 我的日报日历热力图单日状态。
|
||||
type CalendarDay struct {
|
||||
Date string `json:"date"`
|
||||
Status int8 `json:"status"` // 0草稿 1已提交
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
|
||||
// CalendarResult 近期热力图数据(默认近三个月)。
|
||||
type CalendarResult struct {
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Total int `json:"total"`
|
||||
Submitted int `json:"submitted"`
|
||||
Days []CalendarDay `json:"days"`
|
||||
}
|
||||
|
||||
// Calendar 返回最近 months 个月(含本月)本人日报的逐日状态,供热力图按月/周渲染。
|
||||
// months 越界时回落到 3 个月。
|
||||
func Calendar(u *UserContext, months int) (*CalendarResult, error) {
|
||||
if months < 1 || months > 12 {
|
||||
months = 3
|
||||
}
|
||||
now := time.Now()
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
start := monthStart.AddDate(0, -(months - 1), 0)
|
||||
end := monthStart.AddDate(0, 1, -1)
|
||||
startDate := start.Format("2006-01-02")
|
||||
endDate := end.Format("2006-01-02")
|
||||
|
||||
var rows []models.OaDailyReport
|
||||
if _, err := models.Orm.QueryTable(new(models.OaDailyReport)).
|
||||
Filter("tid", u.Tid).
|
||||
Filter("user_id", u.UID).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("report_date__gte", startDate).
|
||||
Filter("report_date__lte", endDate).
|
||||
OrderBy("report_date").
|
||||
All(&rows, "id", "report_date", "status"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &CalendarResult{
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Days: make([]CalendarDay, 0, len(rows)),
|
||||
}
|
||||
for i := range rows {
|
||||
res.Days = append(res.Days, CalendarDay{
|
||||
// DATE 列在 parseTime=true 下会带上时间与时区,必须归一化为 YYYY-MM-DD,
|
||||
// 否则前端按日期匹配热力图格子时永远匹配不到。
|
||||
Date: dateOnly(rows[i].ReportDate),
|
||||
Status: rows[i].Status,
|
||||
ID: rows[i].ID,
|
||||
})
|
||||
if rows[i].Status == 1 {
|
||||
res.Submitted++
|
||||
}
|
||||
}
|
||||
res.Total = len(rows)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// mineSummary 本月提交统计与今天的状态。
|
||||
func mineSummary(u *UserContext) (*MineSummary, error) {
|
||||
now := time.Now()
|
||||
@@ -293,15 +405,85 @@ func GetDetail(u *UserContext, id uint64) (*Detail, error) {
|
||||
}
|
||||
|
||||
author := report.UserID == u.UID
|
||||
canEdit := author && report.Status == 0 && InWindow(report.ReportDate)
|
||||
canWithdraw := author && report.Status == 1 && InWindow(report.ReportDate)
|
||||
// 删除仅限草稿:已提交日报需先撤销提交才能删除。
|
||||
canDelete := report.Status == 0 && (u.IsAdmin || (author && InWindow(report.ReportDate)))
|
||||
|
||||
// 修改痕迹只对作者本人与租户管理员可见。
|
||||
histories := make([]HistoryItem, 0)
|
||||
if author || u.IsAdmin {
|
||||
histories, err = loadHistories(u.Tid, report.ID)
|
||||
if err != nil {
|
||||
// 痕迹表缺失或查询异常时降级为空,避免详情整体 500(部署时需执行 create_oa_dailyreport_history.sql)。
|
||||
histories = make([]HistoryItem, 0)
|
||||
}
|
||||
}
|
||||
return &Detail{
|
||||
Report: *buildItem(report, len(comments)),
|
||||
Comments: comments,
|
||||
CanEdit: author && InWindow(report.ReportDate),
|
||||
CanDelete: (author && InWindow(report.ReportDate)) || u.IsAdmin,
|
||||
CanComment: report.Status == 1 && (author || u.IsAdmin || u.InTeamOrg(report.OrgID)),
|
||||
Report: *buildItem(report, len(comments)),
|
||||
Comments: comments,
|
||||
Histories: histories,
|
||||
IsAuthor: author,
|
||||
CanEdit: canEdit,
|
||||
CanDelete: canDelete,
|
||||
CanComment: report.Status == 1 && (author || u.IsAdmin || u.InTeamOrg(report.OrgID)),
|
||||
CanWithdraw: canWithdraw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Withdraw 撤销提交:把已提交日报退回草稿,并把本次提交的内容快照写入修改痕迹。
|
||||
// 仅作者本人、且日报在补写窗口内时可操作;撤销后需修改并重新提交。
|
||||
func Withdraw(u *UserContext, id uint64) (*ReportItem, error) {
|
||||
report, err := loadReport(u.Tid, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if report.UserID != u.UID {
|
||||
return nil, errForbidden("只能撤销自己提交的日报")
|
||||
}
|
||||
if report.Status != 1 {
|
||||
return nil, errBad("日报尚未提交,无需撤销")
|
||||
}
|
||||
if !InWindow(report.ReportDate) {
|
||||
return nil, errBad(fmt.Sprintf("只能撤销最近%d天(%s 至 %s)的日报", BackfillDays, WindowStart(), Today()))
|
||||
}
|
||||
|
||||
tx, err := models.Orm.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
history := models.OaDailyReportHistory{
|
||||
Tid: u.Tid,
|
||||
ReportID: report.ID,
|
||||
UserID: u.UID,
|
||||
UserName: u.Name,
|
||||
ReportDate: report.ReportDate,
|
||||
Action: 1,
|
||||
TodayDone: report.TodayDone,
|
||||
TomorrowPlan: report.TomorrowPlan,
|
||||
Issues: report.Issues,
|
||||
Attachments: report.Attachments,
|
||||
SubmitTime: report.SubmitTime,
|
||||
}
|
||||
if _, err := tx.Insert(&history); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.QueryTable(new(models.OaDailyReport)).
|
||||
Filter("tid", u.Tid).Filter("id", report.ID).
|
||||
Update(map[string]interface{}{"status": 0, "submit_time": nil}); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report.Status = 0
|
||||
count := commentCount(u.Tid, report.ID)
|
||||
return buildItem(report, count), nil
|
||||
}
|
||||
|
||||
// Delete 软删除日报及其评论:作者限补写窗口内,管理员不受限。
|
||||
//
|
||||
// 日报的 is_deleted 写入记录 ID(而非 1),既标记删除,又让唯一键
|
||||
@@ -311,6 +493,10 @@ func Delete(u *UserContext, id uint64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 已提交日报锁定:不允许直接删除,需先撤销提交退回草稿。
|
||||
if report.Status == 1 {
|
||||
return errBad("日报已提交并锁定,请先撤销提交后再删除")
|
||||
}
|
||||
author := report.UserID == u.UID
|
||||
if !author && !u.IsAdmin {
|
||||
return errForbidden("无权删除该日报")
|
||||
@@ -462,6 +648,47 @@ func buildComment(u *UserContext, r *models.OaDailyReportComment) Comment {
|
||||
}
|
||||
}
|
||||
|
||||
// hasWithdrawHistory 判断一条日报是否被撤销过提交,用于识别“重新提交”。
|
||||
func hasWithdrawHistory(tid, reportID uint64) bool {
|
||||
n, err := models.Orm.QueryTable(new(models.OaDailyReportHistory)).
|
||||
Filter("tid", tid).Filter("report_id", reportID).Filter("action", 1).
|
||||
Count()
|
||||
return err == nil && n > 0
|
||||
}
|
||||
|
||||
// loadHistories 读取一条日报的全部修改痕迹,按写入顺序返回。
|
||||
func loadHistories(tid, reportID uint64) ([]HistoryItem, error) {
|
||||
var rows []models.OaDailyReportHistory
|
||||
if _, err := models.Orm.QueryTable(new(models.OaDailyReportHistory)).
|
||||
Filter("tid", tid).Filter("report_id", reportID).
|
||||
OrderBy("id").All(&rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]HistoryItem, 0, len(rows))
|
||||
for i := range rows {
|
||||
out = append(out, buildHistory(&rows[i]))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildHistory(h *models.OaDailyReportHistory) HistoryItem {
|
||||
attachments := make([]Attachment, 0)
|
||||
if strings.TrimSpace(h.Attachments) != "" {
|
||||
_ = json.Unmarshal([]byte(h.Attachments), &attachments)
|
||||
}
|
||||
return HistoryItem{
|
||||
ID: h.ID,
|
||||
Action: h.Action,
|
||||
UserName: h.UserName,
|
||||
TodayDone: h.TodayDone,
|
||||
TomorrowPlan: h.TomorrowPlan,
|
||||
Issues: h.Issues,
|
||||
Attachments: attachments,
|
||||
SubmitTime: formatTime(h.SubmitTime),
|
||||
CreateTime: h.CreateTime.Format("2006-01-02 15:04"),
|
||||
}
|
||||
}
|
||||
|
||||
func buildItem(r *models.OaDailyReport, commentCount int) *ReportItem {
|
||||
attachments := make([]Attachment, 0)
|
||||
if strings.TrimSpace(r.Attachments) != "" {
|
||||
|
||||
Reference in new issue
Block a user