328 lines
8.5 KiB
Go
328 lines
8.5 KiB
Go
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})
|
||
}
|