优化日报模块

This commit is contained in:
hero920103 committed 2026-10-22 00:27:26 +08:00
1 parent 79d5ae6b66
commit 51fd90fc51
17 files changed
+1767 -257

No files matched your search

+35
View File
@@ -97,6 +97,22 @@ func (c *AppOaController) DailyReportMineList() {
c.oaOk(res)
}
// DailyReportCalendar GET /api/oa/dailyreport/calendar
// 我的日报近期热力图:months 默认 3,返回区间内逐日状态(草稿/已提交)。
func (c *AppOaController) DailyReportCalendar() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
months, _ := c.GetInt("months", 3)
res, err := dailyreport.Calendar(u, months)
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(res)
}
// DailyReportSave POST /api/oa/dailyreport/save
func (c *AppOaController) DailyReportSave() {
u, ok := c.dailyReportCtx()
@@ -152,6 +168,25 @@ func (c *AppOaController) DailyReportDelete() {
c.oaOk(map[string]interface{}{"id": id})
}
// DailyReportWithdraw POST /api/oa/dailyreport/withdraw/:id
// 撤销提交:已提交日报退回草稿并留存本次提交的内容快照,改完需重新提交。
func (c *AppOaController) DailyReportWithdraw() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
id := c.dailyReportID()
if id == 0 {
return
}
item, err := dailyreport.Withdraw(u, id)
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(item)
}
// DailyReportTeamStatus GET /api/oa/dailyreport/team/status
func (c *AppOaController) DailyReportTeamStatus() {
u, ok := c.dailyReportCtx()
+35
View File
@@ -175,6 +175,41 @@ func (c *BackendOaDailyReportController) Delete() {
c.odrOk(map[string]interface{}{"id": id})
}
// Withdraw POST /backend/oa/dailyreport/withdraw/:id
// 撤销提交:已提交日报退回草稿并留存本次提交的内容快照,改完需重新提交。
func (c *BackendOaDailyReportController) Withdraw() {
u, ok := c.odrCtx()
if !ok {
return
}
id := c.odrIDParam(":id")
if id == 0 {
return
}
item, err := dailyreport.Withdraw(u, id)
if err != nil {
c.odrFail(err)
return
}
c.odrOk(item)
}
// Calendar GET /backend/oa/dailyreport/calendar
// 我的日报近期热力图:months 默认 3,返回区间内逐日状态(草稿/已提交),供前端按月、按周渲染。
func (c *BackendOaDailyReportController) Calendar() {
u, ok := c.odrCtx()
if !ok {
return
}
months, _ := c.GetInt("months", 3)
res, err := dailyreport.Calendar(u, months)
if err != nil {
c.odrFail(err)
return
}
c.odrOk(res)
}
// TeamStatus GET /backend/oa/dailyreport/team/status
// 团队日报汇总:date/org_id/keyword/page/pageSize。
func (c *BackendOaDailyReportController) TeamStatus() {
@@ -0,0 +1,38 @@
-- =============================================================
-- OA 工作日报 —— 修改痕迹表
--
-- 对应代码:
-- models/oa_daily_report.go(OaDailyReportHistory)
-- services/dailyreport/report.go(Withdraw / Save / GetDetail)
-- controllers/backend_oa_dailyreport.go(后台 PC 端)
-- controllers/app_oa_dailyreport.go(小程序端)
--
-- 设计要点:
-- 1. 日报提交后锁定(status=1),不允许直接修改;
-- 需要修改时必须先「撤销提交」退回草稿(status=0),改完再重新提交。
-- 2. 每次撤销提交写入一条 action=1 的痕迹,保存本次提交时的内容快照;
-- 撤销后重新提交写入一条 action=2 的痕迹,保存新版本内容快照。
-- 3. 痕迹只增不改:日报删除后痕迹保留(report_id 仍可追溯),不做软删除。
-- 4. 不显式指定 COLLATE,沿用数据库默认排序规则,避免 JOIN 时排序规则冲突。
-- =============================================================
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `yz_backend_oa_daily_report_history` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID',
`report_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '日报ID(yz_backend_oa_daily_report.id)',
`user_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作人uid(yz_system_tenant_user.uid)',
`user_name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '操作人姓名快照',
`report_date` DATE NOT NULL COMMENT '日报日期',
`action` TINYINT NOT NULL DEFAULT 1 COMMENT '1撤销提交 2重新提交',
`today_done` TEXT COMMENT '快照:今日完成',
`tomorrow_plan` TEXT COMMENT '快照:明日计划',
`issues` TEXT COMMENT '快照:问题与建议',
`attachments` TEXT COMMENT '快照:附件JSON数组',
`submit_time` DATETIME DEFAULT NULL COMMENT '快照:提交时间',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_tid_report` (`tid`, `report_id`, `id`),
KEY `idx_tid_date` (`tid`, `report_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA-工作日报修改痕迹';
+2
View File
@@ -195,6 +195,8 @@ func Init(_ string) {
// 工作日报(docs/sql/create_oa_dailyreport.sql)
new(OaDailyReport),
new(OaDailyReportComment),
// 日报修改痕迹(docs/sql/create_oa_dailyreport_history.sql)
new(OaDailyReportHistory),
new(OaDocCategory),
new(OaDoc),
new(OaDocLink),
+37 -11
View File
@@ -6,21 +6,22 @@ import "time"
//
// 一人一天一条:user_id 存 yz_system_tenant_user.uid(不是 JWT 的 identity_id),
// 由 (tid, user_id, report_date, is_deleted) 唯一键保证。
// 草稿 status=0,提交后 status=1;删除为软删除。
// 草稿 status=0,提交后 status=1 并锁定(需撤销提交退回草稿后才能修改);删除为软删除。
// 每次撤销提交 / 撤销后重新提交都会在 yz_backend_oa_daily_report_history 留下内容快照。
//
// IsDeleted 同时承担两个职责:0=未删除;删除时写入记录 ID 作为删除批次标记,
// 这样唯一键在「删掉后同一天重新填写」时不会与历史软删记录冲突。
type OaDailyReport struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
UserID uint64 `orm:"column(user_id)" json:"user_id"`
UserName string `orm:"column(user_name);size(50);default()" json:"user_name"`
OrgID uint64 `orm:"column(org_id);default(0)" json:"org_id"`
OrgName string `orm:"column(org_name);size(100);default()" json:"org_name"`
ReportDate string `orm:"column(report_date);type(date)" json:"report_date"`
TodayDone string `orm:"column(today_done);type(text);null" json:"today_done"`
TomorrowPlan string `orm:"column(tomorrow_plan);type(text);null" json:"tomorrow_plan"`
Issues string `orm:"column(issues);type(text);null" json:"issues"`
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
UserID uint64 `orm:"column(user_id)" json:"user_id"`
UserName string `orm:"column(user_name);size(50);default()" json:"user_name"`
OrgID uint64 `orm:"column(org_id);default(0)" json:"org_id"`
OrgName string `orm:"column(org_name);size(100);default()" json:"org_name"`
ReportDate string `orm:"column(report_date);type(date)" json:"report_date"`
TodayDone string `orm:"column(today_done);type(text);null" json:"today_done"`
TomorrowPlan string `orm:"column(tomorrow_plan);type(text);null" json:"tomorrow_plan"`
Issues string `orm:"column(issues);type(text);null" json:"issues"`
// Attachments 附件JSON数组:[{"id":1,"url":"","name":"","size":0}]
Attachments string `orm:"column(attachments);type(text);null" json:"attachments"`
Status int8 `orm:"column(status);default(1)" json:"status"` // 0草稿 1已提交
@@ -35,6 +36,31 @@ func (m *OaDailyReport) TableName() string {
return "yz_backend_oa_daily_report"
}
// OaDailyReportHistory OA工作日报修改痕迹 yz_backend_oa_daily_report_history
//
// 已提交日报被撤销提交(action=1)时写入本次提交的内容快照;
// 撤销后重新提交(action=2)时写入新版本内容快照,保证「改了有据可查」。
// 痕迹只增不改,随日报一起逻辑上归档,不做软删除。
type OaDailyReportHistory struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
ReportID uint64 `orm:"column(report_id)" json:"report_id"`
UserID uint64 `orm:"column(user_id)" json:"user_id"`
UserName string `orm:"column(user_name);size(50);default()" json:"user_name"`
ReportDate string `orm:"column(report_date);type(date)" json:"report_date"`
Action int8 `orm:"column(action);default(1)" json:"action"` // 1撤销提交 2重新提交
TodayDone string `orm:"column(today_done);type(text);null" json:"today_done"`
TomorrowPlan string `orm:"column(tomorrow_plan);type(text);null" json:"tomorrow_plan"`
Issues string `orm:"column(issues);type(text);null" json:"issues"`
Attachments string `orm:"column(attachments);type(text);null" json:"attachments"`
SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
}
func (m *OaDailyReportHistory) TableName() string {
return "yz_backend_oa_daily_report_history"
}
// OaDailyReportComment OA工作日报评论 yz_backend_oa_daily_report_comment
//
// ReplyToID 指向同表评论ID,实现两级回复;0 表示直接评论日报。
+2
View File
@@ -111,7 +111,9 @@ func Register() {
// OA 工作日报(与 /backend/oa/dailyreport/* 同一套数据;小程序「工作日报」页面使用)
beego.Router("/api/oa/dailyreport/profile", &controllers.AppOaController{}, "get:DailyReportProfile")
beego.Router("/api/oa/dailyreport/mine/list", &controllers.AppOaController{}, "get:DailyReportMineList")
beego.Router("/api/oa/dailyreport/calendar", &controllers.AppOaController{}, "get:DailyReportCalendar")
beego.Router("/api/oa/dailyreport/save", &controllers.AppOaController{}, "post:DailyReportSave")
beego.Router("/api/oa/dailyreport/withdraw/:id", &controllers.AppOaController{}, "post:DailyReportWithdraw")
beego.Router("/api/oa/dailyreport/detail/:id", &controllers.AppOaController{}, "get:DailyReportDetail")
beego.Router("/api/oa/dailyreport/delete/:id", &controllers.AppOaController{}, "delete:DailyReportDelete")
beego.Router("/api/oa/dailyreport/team/status", &controllers.AppOaController{}, "get:DailyReportTeamStatus")
+2
View File
@@ -352,7 +352,9 @@ func RegisterAuthRoutes() {
// OA工作日报(本人 / 部门主管 / 管理员三级可见,支持评论)
beego.Router("/backend/oa/dailyreport/profile", &controllers.BackendOaDailyReportController{}, "get:Profile")
beego.Router("/backend/oa/dailyreport/mine/list", &controllers.BackendOaDailyReportController{}, "get:MineList")
beego.Router("/backend/oa/dailyreport/calendar", &controllers.BackendOaDailyReportController{}, "get:Calendar")
beego.Router("/backend/oa/dailyreport/save", &controllers.BackendOaDailyReportController{}, "post:Save")
beego.Router("/backend/oa/dailyreport/withdraw/:id", &controllers.BackendOaDailyReportController{}, "post:Withdraw")
beego.Router("/backend/oa/dailyreport/detail/:id", &controllers.BackendOaDailyReportController{}, "get:Detail")
beego.Router("/backend/oa/dailyreport/delete/:id", &controllers.BackendOaDailyReportController{}, "delete:Delete")
beego.Router("/backend/oa/dailyreport/team/status", &controllers.BackendOaDailyReportController{}, "get:TeamStatus")
+257 -30
View File
@@ -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) != "" {
+114 -49
View File
@@ -2,6 +2,7 @@ package dailyreport
import (
"math"
"sort"
"strings"
"server/models"
@@ -26,7 +27,7 @@ type TeamStats struct {
Rate float64 `json:"rate"` // 提交率百分比,保留一位小数
}
// TeamRow 团队汇总中的一名成员。
// TeamRow 团队汇总中的一名成员(仅包含当日已提交日报的成员)。
type TeamRow struct {
UserID uint64 `json:"user_id"`
UserName string `json:"user_name"`
@@ -35,9 +36,10 @@ type TeamRow struct {
OrgName string `json:"org_name"`
Submitted bool `json:"submitted"`
ReportID uint64 `json:"report_id"`
Status int8 `json:"status"` // -1未填写 0草稿 1已提交
Status int8 `json:"status"` // 0草稿 1已提交(团队列表只返回 1)
SubmitTime string `json:"submit_time"`
HasIssue bool `json:"has_issue"`
Summary string `json:"summary"` // 日报内容摘要(优先今日完成)
}
// TeamResult 团队汇总结果。
@@ -56,7 +58,8 @@ type OrgOption struct {
MemberCount int `json:"member_count"`
}
// TeamStatus 团队日报汇总:按日期返回管辖范围内每位成员的提交情况。
// TeamStatus 团队日报日志池:只返回当日已提交的日报,未提交(含仅存草稿)的成员不展示。
// 列表按提交时间倒序排列(按“谁发布了内容”展示,不按人分组),右侧根据选中项展示日报内容。
// 管理员不选部门时统计全租户(含未分配部门的成员)。
func TeamStatus(u *UserContext, q TeamQuery) (*TeamResult, error) {
if !u.CanViewTeam {
@@ -101,73 +104,135 @@ func TeamStatus(u *UserContext, q TeamQuery) (*TeamResult, error) {
return nil, errForbidden("你还没有管辖的部门")
}
cond := orm.NewCondition().
// 管辖范围内的成员:用于姓名/部门补全与提交统计,不受关键词与分页影响。
memberCond := orm.NewCondition().
And("tid", u.Tid).
And("delete_time__isnull", true).
And("status", 1).
AndNot("uid", u.UID)
And("status", 1)
if scopeIDs != nil {
cond = cond.And("org_id__in", scopeIDs)
memberCond = memberCond.And("org_id__in", scopeIDs)
}
if kw := strings.TrimSpace(q.Keyword); kw != "" {
cond = cond.AndCond(orm.NewCondition().
Or("name__icontains", kw).
Or("account__icontains", kw))
}
base := models.Orm.QueryTable(new(models.SystemTenantUser)).SetCond(cond)
total, _ := base.Count()
var members []models.SystemTenantUser
if _, err := base.OrderBy("org_id", "uid").
Limit(q.PageSize).Offset((q.Page - 1) * q.PageSize).
All(&members); err != nil {
return nil, err
}
// 统计口径覆盖全部成员,不受分页影响。
var allMembers []models.SystemTenantUser
if _, err := base.OrderBy("uid").All(&allMembers, "uid"); err != nil {
if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
SetCond(memberCond).OrderBy("org_id", "uid").All(&allMembers); err != nil {
return nil, err
}
memberByUID := make(map[uint64]*models.SystemTenantUser, len(allMembers))
for i := range allMembers {
memberByUID[allMembers[i].Uid] = &allMembers[i]
}
reports, err := loadReportsByDate(u.Tid, date)
if err != nil {
return nil, err
}
stats := TeamStats{Total: len(allMembers)}
for _, m := range allMembers {
if r := reports[m.Uid]; r != nil && r.Status == 1 {
stats.Submitted++
kw := strings.TrimSpace(q.Keyword)
rows := make([]TeamRow, 0, len(allMembers))
for i := range allMembers {
m := &allMembers[i]
r := reports[m.Uid]
if r == nil || r.Status != 1 {
continue
}
stats.Submitted++
row := buildTeamRow(r, m.OrgID, userNameOf(m), ptrString(m.Account), "")
if o := orgByID[m.OrgID]; o != nil {
row.OrgName = o.OrgName
}
if kw != "" && !matchTeamKeyword(row.UserName, row.Account, kw) {
continue
}
rows = append(rows, row)
}
// 作者不在成员表(成员已删除 / 组织映射异常)时用日报快照补齐,
// 保证已提交的内容不会因人员变动而看不到。
for uid, r := range reports {
if r.Status != 1 {
continue
}
if _, ok := memberByUID[uid]; ok {
continue
}
if scopeIDs != nil && !containsID(scopeIDs, r.OrgID) {
continue
}
row := buildTeamRow(r, r.OrgID, r.UserName, "", r.OrgName)
if kw != "" && !matchTeamKeyword(row.UserName, row.Account, kw) {
continue
}
rows = append(rows, row)
}
// 日志池排序:按发布时间(提交时间)倒序,最新提交排在最前。
sort.SliceStable(rows, func(i, j int) bool {
if rows[i].SubmitTime != rows[j].SubmitTime {
return rows[i].SubmitTime > rows[j].SubmitTime
}
return rows[i].ReportID > rows[j].ReportID
})
stats.Missing = stats.Total - stats.Submitted
if stats.Missing < 0 {
stats.Missing = 0
}
if stats.Total > 0 {
stats.Rate = math.Round(float64(stats.Submitted)*1000/float64(stats.Total)) / 10
}
list := make([]TeamRow, 0, len(members))
for i := range members {
m := &members[i]
row := TeamRow{
UserID: m.Uid,
UserName: userNameOf(m),
Account: ptrString(m.Account),
OrgID: m.OrgID,
Status: -1,
}
if o := orgByID[m.OrgID]; o != nil {
row.OrgName = o.OrgName
}
if r := reports[m.Uid]; r != nil {
row.ReportID = r.ID
row.Status = r.Status
row.Submitted = r.Status == 1
row.SubmitTime = formatTime(r.SubmitTime)
row.HasIssue = strings.TrimSpace(r.Issues) != ""
}
list = append(list, row)
// 列表分页:只对已提交的日报分页。
total := len(rows)
start := (q.Page - 1) * q.PageSize
if start > total {
start = total
}
return &TeamResult{Date: date, List: list, Total: total, Stats: stats}, nil
end := start + q.PageSize
if end > total {
end = total
}
return &TeamResult{Date: date, List: rows[start:end], Total: int64(total), Stats: stats}, nil
}
// buildTeamRow 组装一条团队日报行;摘要取日报正文。
func buildTeamRow(r *models.OaDailyReport, orgID uint64, userName, account, orgName string) TeamRow {
return TeamRow{
UserID: r.UserID,
UserName: userName,
Account: account,
OrgID: orgID,
OrgName: orgName,
Submitted: true,
ReportID: r.ID,
Status: r.Status,
SubmitTime: formatTime(r.SubmitTime),
HasIssue: strings.TrimSpace(r.Issues) != "",
Summary: reportSummary(r),
}
}
// reportSummary 生成团队列表用的内容摘要(优先今日完成),超长截断。
func reportSummary(r *models.OaDailyReport) string {
for _, text := range []string{r.TodayDone, r.TomorrowPlan, r.Issues} {
s := strings.TrimSpace(text)
if s == "" {
continue
}
runes := []rune(s)
if len(runes) > 60 {
return string(runes[:60]) + "…"
}
return s
}
return ""
}
// matchTeamKeyword 按姓名/账号模糊匹配团队筛选项。
func matchTeamKeyword(name, account, kw string) bool {
kw = strings.ToLower(kw)
return strings.Contains(strings.ToLower(name), kw) ||
strings.Contains(strings.ToLower(account), kw)
}
// TeamOrgs 返回团队筛选可选部门;管理员为全租户,主管仅自己管辖的部门子树。