主要更新uniapp相关功能
This commit is contained in:
1 parent
aa92ca40a2
commit
5b4dbb2b46
65 files changed
+8532
-1921
No files matched your search
@@ -0,0 +1,726 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// AppOaController 移动端 OA 协同办公接口(通知公告 / 文档库 / 薪酬工资单 / 员工档案)。
|
||||
//
|
||||
// 与 Web 管理端(/backend/oa/*)的区别:
|
||||
// - 鉴权放宽到 app 令牌(backend/app/platform),供 uniapp 调用;
|
||||
// - 只提供读能力(浏览 / 下载 / 收藏);写入操作仍留在 Web 端;
|
||||
// - 薪酬工资单与员工档案仅返回「本人」数据,通过登录账号匹配员工。
|
||||
//
|
||||
// 数据隔离:一律以 JWT 中的 TenantId 过滤,前端无法指定租户。
|
||||
type AppOaController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AppOaController) oaClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims.UserType != "backend" && claims.UserType != "app" && claims.UserType != "platform" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *AppOaController) oaErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *AppOaController) oaOk(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// oaMyEmployee 解析当前登录用户对应的员工记录(tid + 账号匹配,口径与 backend_auth 一致)。
|
||||
func (c *AppOaController) oaMyEmployee(claims *jwtutil.Claims) (*models.BackendEmployee, bool) {
|
||||
uid := services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID))
|
||||
account := strings.TrimSpace(claims.Username)
|
||||
var tu models.SystemTenantUser
|
||||
if models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).Filter("tid", claims.TenantId).
|
||||
Filter("delete_time__isnull", true).One(&tu) == nil {
|
||||
if tu.Account != nil && strings.TrimSpace(*tu.Account) != "" {
|
||||
account = strings.TrimSpace(*tu.Account)
|
||||
}
|
||||
}
|
||||
if account == "" {
|
||||
return nil, false
|
||||
}
|
||||
var emp models.BackendEmployee
|
||||
if models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("tid", claims.TenantId).Filter("account", account).
|
||||
Filter("delete_time__isnull", true).One(&emp) != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &emp, true
|
||||
}
|
||||
|
||||
// ---------------- 通知公告 ----------------
|
||||
|
||||
// noticeItem 公告列表项(正文只回传摘要,避免列表过大)。
|
||||
func noticeItem(n models.OaNotice) map[string]interface{} {
|
||||
publishTime := ""
|
||||
if n.PublishTime != nil {
|
||||
publishTime = n.PublishTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": n.ID,
|
||||
"title": n.Title,
|
||||
"notice_type": n.NoticeType,
|
||||
"level": n.Level,
|
||||
"is_top": n.IsTop,
|
||||
"publisher_name": n.PublisherName,
|
||||
"publish_time": publishTime,
|
||||
"read_count": n.ReadCount,
|
||||
"summary": oaTextSummary(n.Content, 60),
|
||||
}
|
||||
}
|
||||
|
||||
// oaTextSummary 取正文纯文本摘要,按「字符数」截断(中文安全)。
|
||||
func oaTextSummary(content string, limit int) string {
|
||||
text := strings.Join(strings.Fields(content), " ")
|
||||
runes := []rune(text)
|
||||
if limit > 0 && len(runes) > limit {
|
||||
return string(runes[:limit]) + "…"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// NoticeList GET /app/oa/notice/list 仅返回已发布(status=1)的公告。
|
||||
func (c *AppOaController) NoticeList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.OaNotice)).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("status", 1)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
var rows []models.OaNotice
|
||||
_, err = qs.OrderBy("-is_top", "-publish_time", "-id").
|
||||
Limit(pageSize).Offset((page - 1) * pageSize).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, n := range rows {
|
||||
list = append(list, noticeItem(n))
|
||||
}
|
||||
|
||||
c.oaOk(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// NoticeDetail GET /app/oa/notice/:id 公告详情,同时累加阅读数。
|
||||
func (c *AppOaController) NoticeDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的公告ID")
|
||||
return
|
||||
}
|
||||
|
||||
var n models.OaNotice
|
||||
if err := models.Orm.QueryTable(new(models.OaNotice)).
|
||||
Filter("id", id).Filter("tid", claims.TenantId).
|
||||
Filter("is_deleted", 0).Filter("status", 1).One(&n); err != nil {
|
||||
c.oaErr(404, 404, "公告不存在")
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = models.Orm.Raw(
|
||||
"UPDATE yz_backend_oa_notice SET read_count = read_count + 1 WHERE id = ?", id).Exec()
|
||||
n.ReadCount++
|
||||
|
||||
item := noticeItem(n)
|
||||
item["content"] = n.Content
|
||||
c.oaOk(item)
|
||||
}
|
||||
|
||||
// ---------------- 文档库 ----------------
|
||||
|
||||
// oaDocActor 由 JWT 构造文档访问者上下文(租户 + 用户可见性)。
|
||||
func (c *AppOaController) oaDocActor(claims *jwtutil.Claims) services.OaDocActor {
|
||||
return services.NewOaDocActor(claims.TenantId, uint64(claims.UserID))
|
||||
}
|
||||
|
||||
// docItem 文档列表/详情项(不回传体积较大的在线编辑源)。
|
||||
func docItem(d models.OaDoc) map[string]interface{} {
|
||||
updateTime := ""
|
||||
if d.UpdateTime != nil {
|
||||
updateTime = d.UpdateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": d.ID,
|
||||
"title": d.Title,
|
||||
"category_id": d.CategoryID,
|
||||
"file_url": d.FileURL,
|
||||
"file_name": d.FileName,
|
||||
"ext": d.Ext,
|
||||
"size": d.Size,
|
||||
"doc_type": d.DocType,
|
||||
"tags": d.Tags,
|
||||
"summary": d.Summary,
|
||||
"is_online": d.IsOnline,
|
||||
"status": d.Status,
|
||||
"version": d.Version,
|
||||
"is_star": d.IsStar,
|
||||
"owner_name": d.OwnerName,
|
||||
"creator_name": d.CreatorName,
|
||||
"view_count": d.ViewCount,
|
||||
"download_count": d.DownCount,
|
||||
"visibility": d.Visibility,
|
||||
"create_time": d.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": updateTime,
|
||||
}
|
||||
}
|
||||
|
||||
// DocCategories GET /app/oa/document/categories?scope=shared|personal|all 分类树(含数量)。
|
||||
func (c *AppOaController) DocCategories() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims), strings.TrimSpace(c.GetString("scope")))
|
||||
if err != nil {
|
||||
c.oaErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.oaOk(tree)
|
||||
}
|
||||
|
||||
// DocList GET /app/oa/document/list 文档分页(按可见性 + 空间过滤)。
|
||||
func (c *AppOaController) DocList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
status, _ := c.GetInt("status", -1)
|
||||
docType, _ := c.GetInt("doc_type", -1)
|
||||
star, _ := c.GetInt("star", -1)
|
||||
categoryID, _ := c.GetUint64("category_id")
|
||||
|
||||
actor := c.oaDocActor(claims)
|
||||
list, total, err := services.OaDocList(actor, services.OaDocListParams{
|
||||
Actor: actor,
|
||||
Scope: strings.TrimSpace(c.GetString("scope")),
|
||||
Keyword: strings.TrimSpace(c.GetString("keyword")),
|
||||
CategoryID: categoryID,
|
||||
Uncategorized: strings.TrimSpace(c.GetString("uncategorized")) == "1",
|
||||
Visibility: -1,
|
||||
Mine: strings.TrimSpace(c.GetString("mine")) == "1",
|
||||
Status: status,
|
||||
DocType: docType,
|
||||
Star: star,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.oaErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(list))
|
||||
for _, d := range list {
|
||||
items = append(items, docItem(d))
|
||||
}
|
||||
c.oaOk(map[string]interface{}{"list": items, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// DocDetail GET /app/oa/document/:id 文档详情(累加查看数)。
|
||||
func (c *AppOaController) DocDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的文档ID")
|
||||
return
|
||||
}
|
||||
doc, err := services.OaDocView(c.oaDocActor(claims), id)
|
||||
if err != nil {
|
||||
c.oaErr(404, 404, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(doc.Content) != "" {
|
||||
doc.IsOnline = 1
|
||||
}
|
||||
c.oaOk(docItem(*doc))
|
||||
}
|
||||
|
||||
// DocDownload GET /app/oa/document/download/:id 记录下载并返回下载地址。
|
||||
func (c *AppOaController) DocDownload() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的文档ID")
|
||||
return
|
||||
}
|
||||
doc, err := services.OaDocDownload(c.oaDocActor(claims), id)
|
||||
if err != nil {
|
||||
c.oaErr(404, 404, err.Error())
|
||||
return
|
||||
}
|
||||
c.oaOk(map[string]interface{}{
|
||||
"id": doc.ID,
|
||||
"title": doc.Title,
|
||||
"file_url": doc.FileURL,
|
||||
"file_name": doc.FileName,
|
||||
"download_count": doc.DownCount,
|
||||
})
|
||||
}
|
||||
|
||||
// DocStar POST /app/oa/document/:id/star 收藏 / 取消收藏。
|
||||
func (c *AppOaController) DocStar() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的文档ID")
|
||||
return
|
||||
}
|
||||
isStar, err := services.OaDocToggleStar(c.oaDocActor(claims), id)
|
||||
if err != nil {
|
||||
c.oaErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.oaOk(map[string]interface{}{"is_star": isStar})
|
||||
}
|
||||
|
||||
// ---------------- 薪酬工资单(仅本人) ----------------
|
||||
|
||||
// PayrollList GET /app/oa/compensation/payrolls 仅返回当前登录用户本人的薪资单。
|
||||
func (c *AppOaController) PayrollList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
emp, ok := c.oaMyEmployee(claims)
|
||||
if !ok {
|
||||
c.oaOk(map[string]interface{}{"list": []map[string]interface{}{}, "total": 0})
|
||||
return
|
||||
}
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.BackendOaPayroll)).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("employee_id", emp.ID).
|
||||
Filter("is_deleted", 0)
|
||||
if month := strings.TrimSpace(c.GetString("payroll_month")); compensationMonthPattern.MatchString(month) {
|
||||
qs = qs.Filter("payroll_month", month)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
var rows []models.BackendOaPayroll
|
||||
if _, err := qs.OrderBy("-payroll_month", "-id").
|
||||
Limit(pageSize).Offset((page - 1) * pageSize).All(&rows); err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, payrollMap(row))
|
||||
}
|
||||
|
||||
c.oaOk(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// PayrollDetail GET /app/oa/compensation/payrolls/:id 本人薪资单明细(含自定义增减项)。
|
||||
func (c *AppOaController) PayrollDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的薪资单ID")
|
||||
return
|
||||
}
|
||||
|
||||
emp, ok := c.oaMyEmployee(claims)
|
||||
if !ok {
|
||||
c.oaErr(404, 404, "未找到您的员工信息")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.BackendOaPayroll
|
||||
if models.Orm.QueryTable(new(models.BackendOaPayroll)).
|
||||
Filter("id", id).Filter("tid", claims.TenantId).
|
||||
Filter("employee_id", emp.ID).Filter("is_deleted", 0).One(&row) != nil {
|
||||
c.oaErr(404, 404, "薪资单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var items []models.BackendOaPayrollItem
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendOaPayrollItem)).
|
||||
Filter("tid", claims.TenantId).Filter("payroll_id", id).
|
||||
Filter("is_deleted", 0).OrderBy("sort_order", "id").All(&items)
|
||||
if items == nil {
|
||||
items = []models.BackendOaPayrollItem{}
|
||||
}
|
||||
|
||||
data := payrollMap(row)
|
||||
data["items"] = items
|
||||
c.oaOk(data)
|
||||
}
|
||||
|
||||
// ---------------- 员工档案(仅本人) ----------------
|
||||
|
||||
// MyEmployeeFile GET /app/oa/employee-file 返回当前登录用户本人的档案与子记录(只读)。
|
||||
func (c *AppOaController) MyEmployeeFile() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
emp, ok := c.oaMyEmployee(claims)
|
||||
if !ok {
|
||||
c.oaErr(404, 404, "未找到您的员工信息")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
var file models.BackendEmployeeFile
|
||||
if models.Orm.QueryTable(new(models.BackendEmployeeFile)).
|
||||
Filter("tid", tid).Filter("is_deleted", 0).
|
||||
Filter("employee_id", emp.ID).One(&file) != nil {
|
||||
c.oaErr(404, 404, "您暂无员工档案")
|
||||
return
|
||||
}
|
||||
|
||||
var records []models.BackendEmployeeFileRecord
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
||||
Filter("tid", tid).Filter("is_deleted", 0).
|
||||
Filter("employee_id", emp.ID).OrderBy("sort", "id").All(&records)
|
||||
if records == nil {
|
||||
records = []models.BackendEmployeeFileRecord{}
|
||||
}
|
||||
|
||||
dtos := (&BackendEmployeeFileController{}).efAssembleDTOList(tid, []models.BackendEmployeeFile{file})
|
||||
|
||||
c.oaOk(map[string]interface{}{"file": dtos[0], "records": records})
|
||||
}
|
||||
|
||||
// ---------------- 工作台仪表盘(移动端首屏聚合) ----------------
|
||||
|
||||
// Dashboard GET /app/oa/dashboard
|
||||
//
|
||||
// 一次返回移动端 OA 首页顶部仪表盘所需的全部数据,口径对齐 Web 端
|
||||
// /backend/oa/schedule/stats 与 /backend/reimbursements/dashboard:
|
||||
//
|
||||
// {
|
||||
// schedule_stats:{today_total,today_done,week_total,week_done,month_total,month_done,overdue},
|
||||
// reimburse:{week,month,year,trend,status_dist,expense_types},
|
||||
// notices:[...], // 最近已发布公告(置顶优先,最多 5 条)
|
||||
// today_schedules:[...] // 今日日程(含已完成,灰显沉底)
|
||||
// }
|
||||
func (c *AppOaController) Dashboard() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
uid := uint64(claims.UserID)
|
||||
|
||||
c.oaOk(map[string]interface{}{
|
||||
"schedule_stats": c.oaDashboardScheduleStats(claims.TenantId, uid),
|
||||
"reimburse": c.oaDashboardReimburse(uint64(claims.TenantId), uid),
|
||||
"notices": c.oaDashboardNotices(claims.TenantId),
|
||||
"today_schedules": c.oaDashboardTodaySchedules(claims.TenantId, uid),
|
||||
})
|
||||
}
|
||||
|
||||
// oaDashboardScheduleStats 今日/本周/本月日程统计与逾期数(与 Web 端 Stats 同口径)。
|
||||
func (c *AppOaController) oaDashboardScheduleStats(tid int, uid uint64) map[string]interface{} {
|
||||
now := time.Now()
|
||||
today := now.Format("2006-01-02")
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
weekStart := now.AddDate(0, 0, -(weekday - 1)).Format("2006-01-02")
|
||||
weekEnd := now.AddDate(0, 0, 7-weekday).Format("2006-01-02")
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
|
||||
base := func() orm.QuerySeter {
|
||||
return models.Orm.QueryTable(new(models.OaSchedule)).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("tid", tid).
|
||||
Filter("user_id", uid)
|
||||
}
|
||||
|
||||
todayTotal, _ := base().Filter("schedule_date", today).Count()
|
||||
todayDone, _ := base().Filter("schedule_date", today).Filter("status", 1).Count()
|
||||
weekTotal, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Count()
|
||||
weekDone, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Filter("status", 1).Count()
|
||||
monthTotal, _ := base().Filter("schedule_date__gte", monthStart).Count()
|
||||
monthDone, _ := base().Filter("schedule_date__gte", monthStart).Filter("status", 1).Count()
|
||||
overdue, _ := base().Filter("schedule_date__lt", today).Filter("status", 0).Count()
|
||||
|
||||
return map[string]interface{}{
|
||||
"today_total": todayTotal,
|
||||
"today_done": todayDone,
|
||||
"week_total": weekTotal,
|
||||
"week_done": weekDone,
|
||||
"month_total": monthTotal,
|
||||
"month_done": monthDone,
|
||||
"overdue": overdue,
|
||||
}
|
||||
}
|
||||
|
||||
// oaDashboardTodaySchedules 今日日程列表(未完成在前,已完成灰显沉底)。
|
||||
func (c *AppOaController) oaDashboardTodaySchedules(tid int, uid uint64) []map[string]interface{} {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var rows []models.OaSchedule
|
||||
_, _ = models.Orm.QueryTable(new(models.OaSchedule)).
|
||||
Filter("is_deleted", 0).Filter("tid", tid).Filter("user_id", uid).
|
||||
Filter("schedule_date", today).
|
||||
OrderBy("status", "start_time", "id").Limit(50).All(&rows)
|
||||
out := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, s := range rows {
|
||||
out = append(out, map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"title": s.Title,
|
||||
"schedule_date": s.ScheduleDate,
|
||||
"start_time": s.StartTime,
|
||||
"end_time": s.EndTime,
|
||||
"all_day": s.AllDay,
|
||||
"color": s.Color,
|
||||
"priority": s.Priority,
|
||||
"status": s.Status,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// oaDashboardNotices 最近已发布公告(置顶优先,最多 5 条)。
|
||||
func (c *AppOaController) oaDashboardNotices(tid int) []map[string]interface{} {
|
||||
var rows []models.OaNotice
|
||||
_, _ = models.Orm.QueryTable(new(models.OaNotice)).
|
||||
Filter("is_deleted", 0).Filter("tid", tid).Filter("status", 1).
|
||||
OrderBy("-is_top", "-publish_time", "-id").Limit(5).All(&rows)
|
||||
out := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, n := range rows {
|
||||
out = append(out, noticeItem(n))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// oaDashboardReimburse 报销统计(周/月/年 + 近6月趋势 + 状态分布 + 类型分析),
|
||||
// 个人报销单量小,一次全量查询内存聚合(与 Web 端 Dashboard 同口径)。
|
||||
func (c *AppOaController) oaDashboardReimburse(tid, uid uint64) map[string]interface{} {
|
||||
now := time.Now()
|
||||
y, m, _ := now.Date()
|
||||
today := time.Date(y, m, now.Day(), 0, 0, 0, 0, now.Location())
|
||||
tomorrow := today.AddDate(0, 0, 1)
|
||||
weekday := int(today.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
weekStart := today.AddDate(0, 0, -(weekday - 1))
|
||||
monthStart := time.Date(y, m, 1, 0, 0, 0, 0, now.Location())
|
||||
yearStart := time.Date(y, 1, 1, 0, 0, 0, 0, now.Location())
|
||||
|
||||
newStat := func() map[string]interface{} {
|
||||
return map[string]interface{}{"total": 0.0, "paid": 0.0, "unpaid": 0.0, "count": 0}
|
||||
}
|
||||
addTo := func(stat map[string]interface{}, row *models.BackendReimbursement) {
|
||||
stat["total"] = stat["total"].(float64) + row.TotalAmount
|
||||
stat["count"] = stat["count"].(int) + 1
|
||||
if row.Status == 5 {
|
||||
stat["paid"] = stat["paid"].(float64) + row.TotalAmount
|
||||
} else {
|
||||
stat["unpaid"] = stat["unpaid"].(float64) + row.TotalAmount
|
||||
}
|
||||
}
|
||||
inRange := func(t, start, end time.Time) bool {
|
||||
return !t.Before(start) && t.Before(end)
|
||||
}
|
||||
|
||||
weekStat, monthStat, yearStat := newStat(), newStat(), newStat()
|
||||
monthKeys := make([]string, 0, 6)
|
||||
monthStats := map[string]map[string]interface{}{}
|
||||
for i := 5; i >= 0; i-- {
|
||||
key := time.Date(y, m-time.Month(i), 1, 0, 0, 0, 0, now.Location()).Format("2006-01")
|
||||
monthKeys = append(monthKeys, key)
|
||||
monthStats[key] = newStat()
|
||||
}
|
||||
statusNames := map[int8]string{0: "草稿", 1: "审批中", 2: "已通过", 3: "已驳回", 4: "已撤回", 5: "已打款"}
|
||||
statusDist := map[string]interface{}{}
|
||||
|
||||
var rows []models.BackendReimbursement
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendReimbursement)).
|
||||
Filter("tid", tid).Filter("user_id", uid).Filter("is_deleted", 0).
|
||||
All(&rows); err != nil {
|
||||
return map[string]interface{}{
|
||||
"week": weekStat, "month": monthStat, "year": yearStat,
|
||||
"trend": []map[string]interface{}{}, "status_dist": statusDist, "expense_types": []map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
for i := range rows {
|
||||
row := &rows[i]
|
||||
created := row.CreateTime
|
||||
if inRange(created, weekStart, tomorrow) {
|
||||
addTo(weekStat, row)
|
||||
}
|
||||
if inRange(created, monthStart, tomorrow) {
|
||||
addTo(monthStat, row)
|
||||
}
|
||||
if inRange(created, yearStart, tomorrow) {
|
||||
addTo(yearStat, row)
|
||||
}
|
||||
if st, ok := monthStats[created.Format("2006-01")]; ok {
|
||||
addTo(st, row)
|
||||
}
|
||||
sk := strconv.Itoa(int(row.Status))
|
||||
dist, ok := statusDist[sk].(map[string]interface{})
|
||||
if !ok {
|
||||
dist = map[string]interface{}{"name": statusNames[row.Status], "count": 0, "amount": 0.0}
|
||||
statusDist[sk] = dist
|
||||
}
|
||||
dist["count"] = dist["count"].(int) + 1
|
||||
dist["amount"] = dist["amount"].(float64) + row.TotalAmount
|
||||
}
|
||||
|
||||
trend := make([]map[string]interface{}, 0, len(monthKeys))
|
||||
for _, key := range monthKeys {
|
||||
st := monthStats[key]
|
||||
trend = append(trend, map[string]interface{}{
|
||||
"month": key, "total": st["total"], "paid": st["paid"],
|
||||
"unpaid": st["unpaid"], "count": st["count"],
|
||||
})
|
||||
}
|
||||
|
||||
// 报销类型分析:仅统计已打款(status=5)的报销单费用明细
|
||||
paidIDs := map[uint64]bool{}
|
||||
for i := range rows {
|
||||
if rows[i].Status == 5 {
|
||||
paidIDs[rows[i].ID] = true
|
||||
}
|
||||
}
|
||||
expenseTypes := []map[string]interface{}{}
|
||||
var items []models.BackendReimbursementItem
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("tid", tid).Filter("uid", uid).Filter("is_deleted", 0).
|
||||
All(&items); err == nil {
|
||||
typeAgg := map[string]*struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
for i := range items {
|
||||
if !paidIDs[items[i].ReimbursementID] {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(items[i].ExpenseType)
|
||||
if name == "" {
|
||||
name = "未分类"
|
||||
}
|
||||
agg, ok := typeAgg[name]
|
||||
if !ok {
|
||||
agg = &struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
typeAgg[name] = agg
|
||||
}
|
||||
agg.count++
|
||||
agg.amount += items[i].Amount
|
||||
}
|
||||
for name, agg := range typeAgg {
|
||||
expenseTypes = append(expenseTypes, map[string]interface{}{
|
||||
"name": name, "count": agg.count, "amount": agg.amount,
|
||||
})
|
||||
}
|
||||
sort.Slice(expenseTypes, func(i, j int) bool {
|
||||
return expenseTypes[i]["amount"].(float64) > expenseTypes[j]["amount"].(float64)
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"week": weekStat, "month": monthStat, "year": yearStat,
|
||||
"trend": trend, "status_dist": statusDist, "expense_types": expenseTypes,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user