优化小程序报销模块

This commit is contained in:
hero920103 committed 2026-10-10 14:24:44 +08:00
1 parent 1e9666cefd
commit 48f3bd997c
21 files changed
+5738 -1750

No files matched your search

+327
View File
@@ -0,0 +1,327 @@
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})
}