796 lines
24 KiB
Go
796 lines
24 KiB
Go
package dailyreport
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"server/models"
|
|
|
|
"github.com/beego/beego/v2/client/orm"
|
|
)
|
|
|
|
// Attachment 日报附件,与 PC / 小程序上传接口返回的文件信息对齐。
|
|
type Attachment struct {
|
|
ID uint64 `json:"id"`
|
|
URL string `json:"url"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// Comment 日报评论(两级回复)。
|
|
type Comment struct {
|
|
ID uint64 `json:"id"`
|
|
UserID uint64 `json:"user_id"`
|
|
UserName string `json:"user_name"`
|
|
Content string `json:"content"`
|
|
ReplyToID uint64 `json:"reply_to_id"`
|
|
ReplyToName string `json:"reply_to_name"`
|
|
CreateTime string `json:"create_time"`
|
|
IsMine bool `json:"is_mine"`
|
|
CanDelete bool `json:"can_delete"`
|
|
}
|
|
|
|
// Detail 日报详情:主体 + 评论 + 修改痕迹 + 当前用户可执行的操作。
|
|
type Detail struct {
|
|
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 表示提交,否则保存草稿。
|
|
type SaveInput struct {
|
|
Date string `json:"date"`
|
|
TodayDone string `json:"today_done"`
|
|
TomorrowPlan string `json:"tomorrow_plan"`
|
|
Issues string `json:"issues"`
|
|
Attachments string `json:"attachments"`
|
|
Submit bool `json:"submit"`
|
|
}
|
|
|
|
// MineQuery 我的日报列表查询。
|
|
type MineQuery struct {
|
|
StartDate string
|
|
EndDate string
|
|
Page int
|
|
PageSize int
|
|
}
|
|
|
|
// MineSummary 我的日报页头统计。
|
|
type MineSummary struct {
|
|
MonthTotal int `json:"month_total"`
|
|
MonthSubmitted int `json:"month_submitted"`
|
|
TodayStatus int8 `json:"today_status"` // -1=今天未填写
|
|
TodayReportID uint64 `json:"today_report_id"`
|
|
WindowStart string `json:"window_start"`
|
|
WindowEnd string `json:"window_end"`
|
|
}
|
|
|
|
// MineResult 我的日报列表结果。
|
|
type MineResult struct {
|
|
List []ReportItem `json:"list"`
|
|
Total int64 `json:"total"`
|
|
Summary MineSummary `json:"summary"`
|
|
}
|
|
|
|
// Save 新建或更新一条日报(同一人同一天唯一)。
|
|
// 已提交的日报处于锁定状态,必须先撤销提交退回草稿才能修改。
|
|
func Save(u *UserContext, in SaveInput) (*ReportItem, error) {
|
|
date, err := ParseDate(in.Date)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !InWindow(date) {
|
|
return nil, errBad(fmt.Sprintf("只能补写最近%d天(%s 至 %s)的日报", BackfillDays, WindowStart(), Today()))
|
|
}
|
|
|
|
in.TodayDone = strings.TrimSpace(in.TodayDone)
|
|
in.TomorrowPlan = strings.TrimSpace(in.TomorrowPlan)
|
|
in.Issues = strings.TrimSpace(in.Issues)
|
|
for name, text := range map[string]string{
|
|
"今日完成": in.TodayDone,
|
|
"明日计划": in.TomorrowPlan,
|
|
"问题与建议": in.Issues,
|
|
} {
|
|
if utf8.RuneCountInString(text) > 5000 {
|
|
return nil, errBad(name + "不能超过5000字")
|
|
}
|
|
}
|
|
if in.TodayDone == "" && in.TomorrowPlan == "" && in.Issues == "" {
|
|
return nil, errBad("请至少填写今日完成、明日计划、问题与建议中的一项")
|
|
}
|
|
attachmentJSON, _, err := normalizeAttachments(in.Attachments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var existing models.OaDailyReport
|
|
findErr := models.Orm.QueryTable(new(models.OaDailyReport)).
|
|
Filter("tid", u.Tid).
|
|
Filter("user_id", u.UID).
|
|
Filter("report_date", date).
|
|
Filter("is_deleted", 0).
|
|
One(&existing)
|
|
|
|
status := int8(0)
|
|
if in.Submit {
|
|
status = 1
|
|
}
|
|
now := time.Now()
|
|
|
|
if findErr == orm.ErrNoRows {
|
|
report := models.OaDailyReport{
|
|
Tid: u.Tid,
|
|
UserID: u.UID,
|
|
UserName: u.Name,
|
|
OrgID: u.OrgID,
|
|
OrgName: u.OrgName,
|
|
ReportDate: date,
|
|
TodayDone: in.TodayDone,
|
|
TomorrowPlan: in.TomorrowPlan,
|
|
Issues: in.Issues,
|
|
Attachments: attachmentJSON,
|
|
Status: status,
|
|
}
|
|
if status == 1 {
|
|
report.SubmitTime = &now
|
|
}
|
|
if _, err := models.Orm.Insert(&report); err != nil {
|
|
return nil, errBad("保存失败,该日期可能已存在日报,请刷新后重试")
|
|
}
|
|
return buildItem(&report, 0), nil
|
|
}
|
|
if findErr != nil {
|
|
return nil, findErr
|
|
}
|
|
|
|
// 已提交的日报锁定:必须先撤销提交退回草稿,才能修改内容。
|
|
if existing.Status == 1 {
|
|
return nil, errBad("日报已提交并锁定,如需修改请先撤销提交")
|
|
}
|
|
// 撤销过提交的日报再次提交时,留下重新提交痕迹。
|
|
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
|
|
existing.TomorrowPlan = in.TomorrowPlan
|
|
existing.Issues = in.Issues
|
|
existing.Attachments = attachmentJSON
|
|
existing.Status = status
|
|
existing.UserName = u.Name
|
|
existing.OrgID = u.OrgID
|
|
existing.OrgName = u.OrgName
|
|
|
|
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)
|
|
return buildItem(&existing, count), nil
|
|
}
|
|
|
|
// ListMine 我的日报分页列表 + 页头统计。
|
|
func ListMine(u *UserContext, q MineQuery) (*MineResult, error) {
|
|
if q.Page < 1 {
|
|
q.Page = 1
|
|
}
|
|
if q.PageSize < 1 || q.PageSize > 100 {
|
|
q.PageSize = 20
|
|
}
|
|
|
|
base := models.Orm.QueryTable(new(models.OaDailyReport)).
|
|
Filter("tid", u.Tid).
|
|
Filter("user_id", u.UID).
|
|
Filter("is_deleted", 0)
|
|
|
|
start, err := optionalDate(q.StartDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
end, err := optionalDate(q.EndDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if start != "" {
|
|
base = base.Filter("report_date__gte", start)
|
|
}
|
|
if end != "" {
|
|
base = base.Filter("report_date__lte", end)
|
|
}
|
|
|
|
total, _ := base.Count()
|
|
var rows []models.OaDailyReport
|
|
if _, err := base.OrderBy("-report_date", "-id").
|
|
Limit(q.PageSize).Offset((q.Page - 1) * q.PageSize).
|
|
All(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]uint64, 0, len(rows))
|
|
for i := range rows {
|
|
ids = append(ids, rows[i].ID)
|
|
}
|
|
counts := commentCounts(u.Tid, ids)
|
|
|
|
list := make([]ReportItem, 0, len(rows))
|
|
for i := range rows {
|
|
list = append(list, *buildItem(&rows[i], counts[rows[i].ID]))
|
|
}
|
|
|
|
summary, err := mineSummary(u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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()
|
|
monthStart := now.Format("2006-01") + "-01"
|
|
monthEnd := now.AddDate(0, 1, -1).Format("2006-01-02")
|
|
|
|
out := &MineSummary{
|
|
TodayStatus: -1,
|
|
WindowStart: WindowStart(),
|
|
WindowEnd: Today(),
|
|
}
|
|
base := models.Orm.QueryTable(new(models.OaDailyReport)).
|
|
Filter("tid", u.Tid).
|
|
Filter("user_id", u.UID).
|
|
Filter("is_deleted", 0)
|
|
out.MonthTotal = int(countWhere(base, "report_date__gte", monthStart, "report_date__lte", monthEnd))
|
|
out.MonthSubmitted = int(countWhere(base.Filter("status", 1), "report_date__gte", monthStart, "report_date__lte", monthEnd))
|
|
|
|
var today models.OaDailyReport
|
|
if err := models.Orm.QueryTable(new(models.OaDailyReport)).
|
|
Filter("tid", u.Tid).Filter("user_id", u.UID).
|
|
Filter("report_date", Today()).
|
|
Filter("is_deleted", 0).One(&today); err == nil {
|
|
out.TodayStatus = today.Status
|
|
out.TodayReportID = today.ID
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// GetDetail 读取日报详情;无权限时返回 403。
|
|
func GetDetail(u *UserContext, id uint64) (*Detail, error) {
|
|
report, err := loadReport(u.Tid, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !canViewReport(u, report) {
|
|
return nil, errForbidden("无权查看该日报")
|
|
}
|
|
|
|
comments := make([]Comment, 0)
|
|
if report.Status == 1 {
|
|
comments, err = loadComments(u, report.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
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,
|
|
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),既标记删除,又让唯一键
|
|
// (tid, user_id, report_date, is_deleted) 在「删除后同一天重新填写」时不冲突。
|
|
func Delete(u *UserContext, id uint64) error {
|
|
report, err := loadReport(u.Tid, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 已提交日报锁定:不允许直接删除,需先撤销提交退回草稿。
|
|
if report.Status == 1 {
|
|
return errBad("日报已提交并锁定,请先撤销提交后再删除")
|
|
}
|
|
author := report.UserID == u.UID
|
|
if !author && !u.IsAdmin {
|
|
return errForbidden("无权删除该日报")
|
|
}
|
|
if author && !u.IsAdmin && !InWindow(report.ReportDate) {
|
|
return errBad(fmt.Sprintf("只能删除最近%d天(%s 至 %s)的日报", BackfillDays, WindowStart(), Today()))
|
|
}
|
|
|
|
now := time.Now()
|
|
tx, err := models.Orm.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.QueryTable(new(models.OaDailyReportComment)).
|
|
Filter("tid", u.Tid).Filter("report_id", report.ID).Filter("is_deleted", 0).
|
|
Update(map[string]interface{}{"is_deleted": 1, "delete_time": now}); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if _, err := tx.QueryTable(new(models.OaDailyReport)).
|
|
Filter("tid", u.Tid).Filter("id", report.ID).
|
|
Update(map[string]interface{}{"is_deleted": report.ID, "delete_time": now}); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// AddComment 对已提交日报发表评论或回复;replyToID=0 表示直接评论。
|
|
func AddComment(u *UserContext, reportID uint64, content string, replyToID uint64) (*Comment, error) {
|
|
content = strings.TrimSpace(content)
|
|
if content == "" {
|
|
return nil, errBad("请输入评论内容")
|
|
}
|
|
if utf8.RuneCountInString(content) > 1000 {
|
|
return nil, errBad("评论不能超过1000字")
|
|
}
|
|
report, err := loadReport(u.Tid, reportID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !canViewReport(u, report) {
|
|
return nil, errForbidden("无权评论该日报")
|
|
}
|
|
if report.Status != 1 {
|
|
return nil, errBad("日报提交后才能评论")
|
|
}
|
|
|
|
replyName := ""
|
|
if replyToID > 0 {
|
|
var parent models.OaDailyReportComment
|
|
if err := models.Orm.QueryTable(new(models.OaDailyReportComment)).
|
|
Filter("tid", u.Tid).Filter("id", replyToID).Filter("report_id", reportID).
|
|
Filter("is_deleted", 0).
|
|
One(&parent); err != nil {
|
|
return nil, errBad("回复的评论不存在")
|
|
}
|
|
replyName = parent.UserName
|
|
}
|
|
|
|
row := models.OaDailyReportComment{
|
|
Tid: u.Tid,
|
|
ReportID: reportID,
|
|
UserID: u.UID,
|
|
UserName: u.Name,
|
|
Content: content,
|
|
ReplyToID: replyToID,
|
|
ReplyToName: replyName,
|
|
}
|
|
if _, err := models.Orm.Insert(&row); err != nil {
|
|
return nil, err
|
|
}
|
|
c := buildComment(u, &row)
|
|
return &c, nil
|
|
}
|
|
|
|
// DeleteComment 软删除评论:评论作者本人或管理员。
|
|
func DeleteComment(u *UserContext, commentID uint64) error {
|
|
var row models.OaDailyReportComment
|
|
if err := models.Orm.QueryTable(new(models.OaDailyReportComment)).
|
|
Filter("tid", u.Tid).Filter("id", commentID).Filter("is_deleted", 0).One(&row); err != nil {
|
|
return errNotFound("评论不存在或已删除")
|
|
}
|
|
if row.UserID != u.UID && !u.IsAdmin {
|
|
return errForbidden("只能删除自己的评论")
|
|
}
|
|
_, err := models.Orm.QueryTable(new(models.OaDailyReportComment)).
|
|
Filter("tid", u.Tid).Filter("id", commentID).Filter("is_deleted", 0).
|
|
Update(map[string]interface{}{"is_deleted": 1, "delete_time": time.Now()})
|
|
return err
|
|
}
|
|
|
|
// ========================== 内部工具 ==========================
|
|
|
|
func loadReport(tid, id uint64) (*models.OaDailyReport, error) {
|
|
var report models.OaDailyReport
|
|
if err := models.Orm.QueryTable(new(models.OaDailyReport)).
|
|
Filter("tid", tid).Filter("id", id).Filter("is_deleted", 0).One(&report); err != nil {
|
|
if err == orm.ErrNoRows {
|
|
return nil, errNotFound("日报不存在或已删除")
|
|
}
|
|
return nil, err
|
|
}
|
|
// 统一成 YYYY-MM-DD,保证 InWindow 与前端日期比较正确。
|
|
report.ReportDate = dateOnly(report.ReportDate)
|
|
return &report, nil
|
|
}
|
|
|
|
// canViewReport 本人 / 管理员 / 作者所在部门的主管;草稿仅本人与管理员可见。
|
|
func canViewReport(u *UserContext, r *models.OaDailyReport) bool {
|
|
if r.UserID == u.UID || u.IsAdmin {
|
|
return true
|
|
}
|
|
if r.Status != 1 {
|
|
return false
|
|
}
|
|
return u.InTeamOrg(r.OrgID)
|
|
}
|
|
|
|
func loadComments(u *UserContext, reportID uint64) ([]Comment, error) {
|
|
var rows []models.OaDailyReportComment
|
|
if _, err := models.Orm.QueryTable(new(models.OaDailyReportComment)).
|
|
Filter("tid", u.Tid).Filter("report_id", reportID).
|
|
Filter("is_deleted", 0).
|
|
OrderBy("id").All(&rows); err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]Comment, 0, len(rows))
|
|
for i := range rows {
|
|
out = append(out, buildComment(u, &rows[i]))
|
|
}
|
|
if out == nil {
|
|
out = []Comment{}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func buildComment(u *UserContext, r *models.OaDailyReportComment) Comment {
|
|
return Comment{
|
|
ID: r.ID,
|
|
UserID: r.UserID,
|
|
UserName: r.UserName,
|
|
Content: r.Content,
|
|
ReplyToID: r.ReplyToID,
|
|
ReplyToName: r.ReplyToName,
|
|
CreateTime: r.CreateTime.Format("2006-01-02 15:04"),
|
|
IsMine: r.UserID == u.UID,
|
|
CanDelete: r.UserID == u.UID || u.IsAdmin,
|
|
}
|
|
}
|
|
|
|
// 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) != "" {
|
|
_ = json.Unmarshal([]byte(r.Attachments), &attachments)
|
|
}
|
|
return &ReportItem{
|
|
ID: r.ID,
|
|
UserID: r.UserID,
|
|
UserName: r.UserName,
|
|
OrgID: r.OrgID,
|
|
OrgName: r.OrgName,
|
|
ReportDate: dateOnly(r.ReportDate),
|
|
TodayDone: r.TodayDone,
|
|
TomorrowPlan: r.TomorrowPlan,
|
|
Issues: r.Issues,
|
|
Attachments: attachments,
|
|
Status: r.Status,
|
|
SubmitTime: formatTime(r.SubmitTime),
|
|
CreateTime: r.CreateTime.Format("2006-01-02 15:04"),
|
|
UpdateTime: formatTime(r.UpdateTime),
|
|
CommentCount: commentCount,
|
|
}
|
|
}
|
|
|
|
func formatTime(t *time.Time) string {
|
|
if t == nil || t.IsZero() {
|
|
return ""
|
|
}
|
|
return t.Format("2006-01-02 15:04")
|
|
}
|
|
|
|
// normalizeAttachments 校验并规范化附件 JSON,最多 20 个。
|
|
func normalizeAttachments(raw string) (string, []Attachment, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" || raw == "null" {
|
|
return "[]", []Attachment{}, nil
|
|
}
|
|
var items []Attachment
|
|
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
|
return "", nil, errBad("附件数据格式错误")
|
|
}
|
|
out := make([]Attachment, 0, len(items))
|
|
for _, it := range items {
|
|
if strings.TrimSpace(it.URL) == "" {
|
|
continue
|
|
}
|
|
if len(out) >= 20 {
|
|
return "", nil, errBad("附件最多20个")
|
|
}
|
|
out = append(out, it)
|
|
}
|
|
data, _ := json.Marshal(out)
|
|
return string(data), out, nil
|
|
}
|
|
|
|
func optionalDate(v string) (string, error) {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
return "", nil
|
|
}
|
|
return ParseDate(v)
|
|
}
|
|
|
|
// countWhere 依次应用列值条件后计数。
|
|
func countWhere(qs orm.QuerySeter, kv ...string) int64 {
|
|
for i := 0; i+1 < len(kv); i += 2 {
|
|
qs = qs.Filter(kv[i], kv[i+1])
|
|
}
|
|
n, _ := qs.Count()
|
|
return n
|
|
}
|
|
|
|
func commentCount(tid, reportID uint64) int {
|
|
counts := commentCounts(tid, []uint64{reportID})
|
|
return counts[reportID]
|
|
}
|
|
|
|
// commentCounts 批量统计评论数,避免列表逐条查询。
|
|
func commentCounts(tid uint64, ids []uint64) map[uint64]int {
|
|
out := make(map[uint64]int, len(ids))
|
|
if len(ids) == 0 {
|
|
return out
|
|
}
|
|
holders := make([]string, 0, len(ids))
|
|
args := make([]interface{}, 0, len(ids)+1)
|
|
args = append(args, tid)
|
|
for _, id := range ids {
|
|
holders = append(holders, "?")
|
|
args = append(args, id)
|
|
}
|
|
var rows []struct {
|
|
ReportID uint64 `orm:"column(report_id)"`
|
|
Cnt int `orm:"column(cnt)"`
|
|
}
|
|
sql := "SELECT report_id, COUNT(*) AS cnt FROM yz_backend_oa_daily_report_comment" +
|
|
" WHERE tid = ? AND report_id IN (" + strings.Join(holders, ",") + ") AND is_deleted = 0 GROUP BY report_id"
|
|
if _, err := models.Orm.Raw(sql, args...).QueryRows(&rows); err != nil {
|
|
return out
|
|
}
|
|
for _, r := range rows {
|
|
out[r.ReportID] = r.Cnt
|
|
}
|
|
return out
|
|
}
|