Files
yunzerwebsiteallinone/go/controllers/app_oa_schedule.go
T

328 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"encoding/json"
"io"
"path/filepath"
"strconv"
"strings"
"server/models"
"server/services"
)
// 移动端 OA 工作日程(日历待办)接口,挂在 AppOaController 上。
//
// 与 Web 管理端 /backend/oa/schedule/* 共用同一套数据(yz_backend_oa_schedule,
// 按 JWT 中 tid + user_id 双重隔离)与核心逻辑(oaSchedule*Core / oaScheduleListData),
// 仅鉴权放宽为 app 令牌(oaClaims)。小程序「工作日程」页面走这里,
// 不再复用「日程提醒」(/api/schedule/*,yz_backend_schedule)的接口。
// oaScheduleIDParam 解析 :id 路由参数,非法返回 0。
func (c *AppOaController) oaScheduleIDParam() uint64 {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil {
return 0
}
return id
}
// oaScheduleReadPayload 读取并校验新建/编辑请求体;失败时直接输出错误响应。
func (c *AppOaController) oaScheduleReadPayload() (oaSchedulePayload, bool) {
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.oaErr(400, 400, "参数错误")
return oaSchedulePayload{}, false
}
payload, msg := parseOaSchedulePayloadBody(raw)
if msg != "" {
c.oaErr(400, 400, msg)
return payload, false
}
return payload, true
}
// ScheduleList GET /api/oa/schedule/list
// 移动端工作日程列表:参数与返回结构与 /backend/oa/schedule/list 一致
// (page/pageSize/keyword/status/priority/start_date/end_date/category,
// 返回 list/total/counts)。
func (c *AppOaController) ScheduleList() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
data, code, msg := oaScheduleListData(claims.TenantId, claims.UserID,
page, pageSize,
strings.TrimSpace(c.GetString("keyword")),
strings.TrimSpace(c.GetString("status")),
strings.TrimSpace(c.GetString("priority")),
strings.TrimSpace(c.GetString("start_date")),
strings.TrimSpace(c.GetString("end_date")),
strings.TrimSpace(c.GetString("category")))
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(data)
}
// ScheduleDetail GET /api/oa/schedule/detail/:id
// 日程详情(移动端详情页使用;Web 端直接由列表渲染,无此接口)。
func (c *AppOaController) ScheduleDetail() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
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).
One(&item)
if err != nil {
c.oaErr(404, 404, "日程不存在")
return
}
item.ScheduleDate = normalizeOaDate(item.ScheduleDate)
c.oaOk(map[string]interface{}{
"item": item,
"images": parseOaScheduleImages(item.Images),
})
}
// ScheduleCreate POST /api/oa/schedule/create 新建日程(可选绑定提醒)。
func (c *AppOaController) ScheduleCreate() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
payload, ok := c.oaScheduleReadPayload()
if !ok {
return
}
item, code, msg := oaScheduleCreateCore(claims, payload)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(item)
}
// ScheduleUpdate POST /api/oa/schedule/update/:id 编辑日程(含提醒重绑)。
func (c *AppOaController) ScheduleUpdate() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
payload, ok := c.oaScheduleReadPayload()
if !ok {
return
}
item, code, msg := oaScheduleUpdateCore(claims, id, payload)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(item)
}
// ScheduleDelete DELETE /api/oa/schedule/delete/:id 软删除日程。
func (c *AppOaController) ScheduleDelete() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
code, msg := oaScheduleDeleteCore(claims, id)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(nil)
}
// ScheduleFinish POST /api/oa/schedule/finish/:id 切换完成状态。
// 方法名不能叫 Finish——那是 beego Controller 的生命周期钩子。
func (c *AppOaController) ScheduleFinish() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
newStatus, code, msg := oaScheduleToggleCore(claims, id)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(map[string]interface{}{"status": newStatus})
}
// oaUploadMaxBytes OA 模块附件上传单个文件大小上限(10MB)。
const oaUploadMaxBytes = 10 << 20
// uploadTenantImage 移动端 OA 附件上传共享实现(日程资料 / 报销发票等复用):
// multipart 字段名 file,支持图片与 PDF,单个文件 ≤10MB;文件落入租户文件管理
// (backend/{tid}/{uid}/...,用户个人范围),返回 {id,url,name} 供业务写引用。
func (c *AppOaController) uploadTenantImage() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
fh, header, err := c.GetFile("file")
if err != nil || fh == nil {
c.oaErr(400, 400, "请选择要上传的文件")
return
}
defer fh.Close()
switch strings.ToLower(filepath.Ext(header.Filename)) {
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".pdf":
default:
c.oaErr(400, 400, "仅支持 JPG/PNG/GIF/WEBP/BMP 图片或 PDF 文件")
return
}
if header.Size > oaUploadMaxBytes {
c.oaErr(400, 400, "文件大小不能超过10MB")
return
}
storage, err := services.GetStorageService()
if err != nil {
c.oaErr(500, 500, "存储服务不可用,请稍后重试")
return
}
staged, err := storage.Stage(fh, header)
if err != nil {
c.oaErr(500, 500, "读取上传文件失败")
return
}
committed := false
defer func() {
// 查重命中或出错时清理临时文件
if !committed && staged != nil {
_ = storage.Discard(staged)
}
}()
tid := uint64(claims.TenantId)
tuid := uint64(claims.UserID)
ctx := services.UploadContext{Source: services.SourceBackend, Tid: tid, Tuid: tuid}
scope := ctx.Scope()
// 同用户上传过同一张图(MD5 相同)直接复用文件记录,不重复落盘
exist, err := services.FindDuplicate(services.SourceBackend, scope, tid, tuid, staged.MD5)
if err != nil {
c.oaErr(500, 500, "查重失败")
return
}
if exist != nil {
committed = true
c.oaOk(map[string]interface{}{"id": exist.ID, "url": exist.Src, "name": exist.Name})
return
}
result, err := storage.Commit(staged, ctx)
if err != nil {
c.oaErr(500, 500, "上传失败,请重试")
return
}
committed = true
fileID, _ := services.CreateFileRecord(services.FileUploadParams{
Source: services.SourceBackend,
Scope: scope,
Tid: tid,
Tuid: tuid,
Uid: tuid,
Name: header.Filename,
Ext: staged.Ext,
Size: uint64(staged.Size),
Src: result.URL,
ObjectKey: result.Key,
Storage: storage.Type(),
MD5: staged.MD5,
})
c.oaOk(map[string]interface{}{"id": fileID, "url": result.URL, "name": header.Filename})
}
// ScheduleUploadImage POST /api/oa/schedule/upload(multipart,字段名 file)
// 上传日程资料图片,共享 uploadTenantImage;随后由 ScheduleImages 把引用写进日程。
func (c *AppOaController) ScheduleUploadImage() {
c.uploadTenantImage()
}
// ScheduleImages POST /api/oa/schedule/images/:id 维护日程资料图片(只存文件引用)。
// 与 Web 后台 SetImages 共用核心逻辑;已完成日程也允许补充资料。
func (c *AppOaController) ScheduleImages() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
var payload struct {
Images []oaScheduleImage `json:"images"`
}
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if len(raw) > 0 && json.Unmarshal(raw, &payload) != nil {
c.oaErr(400, 400, "参数错误")
return
}
images, code, msg := oaScheduleSetImagesCore(claims, id, payload.Images)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(map[string]interface{}{"images": images})
}