优化小程序报销模块

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

+344
View File
@@ -0,0 +1,344 @@
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"server/services"
)
// 移动端 OA 报销管理接口,挂在 AppOaController 上。
//
// 与 Web 管理端 /backend/reimbursements* 共用同一套数据
// (yz_backend_reimbursements / _items / _expense_types,按 JWT 中 tid + user_id
// 双重隔离,仅本人单据可见可操作)与核心逻辑(reimburse*Core 共享函数)。
// 小程序「报销管理」页面走这里,不直接调用 /backend/* 接口。
// expenseReadPayload 读取并校验新建/编辑报销单请求体;失败时直接输出错误响应。
func (c *AppOaController) expenseReadPayload() (reimbursementPayload, bool) {
var payload reimbursementPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &payload) != nil {
c.oaErr(400, 400, "参数错误")
return payload, false
}
return payload, true
}
// expenseReadItemPayload 读取并校验费用明细请求体;失败时直接输出错误响应。
func (c *AppOaController) expenseReadItemPayload() (reimbursementItemPayload, bool) {
var payload reimbursementItemPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &payload) != nil {
c.oaErr(400, 400, "参数错误")
return payload, false
}
return payload, true
}
// ExpenseList GET /api/oa/expense/list
// 移动端报销单列表:参数与返回结构与 /backend/reimbursements 一致
// (page/pageSize/keyword/status,返回 list/total)。
func (c *AppOaController) ExpenseList() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
status, statusErr := c.GetInt("status")
data, code, msg := reimburseListData(claims, page, pageSize,
strings.TrimSpace(c.GetString("keyword")), statusErr == nil, status)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(data)
}
// ExpenseDetail GET /api/oa/expense/detail/:id
// 报销单详情(主单 + 费用明细 + 发票文件)。
func (c *AppOaController) ExpenseDetail() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
data, code, msg := reimburseDetailData(claims, id, scheme, host)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(data)
}
// ExpenseCreate POST /api/oa/expense/create 新建报销单(草稿)。
func (c *AppOaController) ExpenseCreate() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
payload, ok := c.expenseReadPayload()
if !ok {
return
}
id, code, msg := reimburseCreateCore(claims, payload)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(map[string]interface{}{"id": id})
}
// ExpenseUpdate POST /api/oa/expense/update/:id 编辑报销单(仅草稿)。
func (c *AppOaController) ExpenseUpdate() {
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.expenseReadPayload()
if !ok {
return
}
code, msg := reimburseUpdateCore(claims, id, payload)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(map[string]interface{}{"id": id})
}
// ExpenseDelete DELETE /api/oa/expense/delete/:id 删除报销单(仅草稿)。
func (c *AppOaController) ExpenseDelete() {
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 := reimburseDeleteCore(claims, id)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(nil)
}
// ExpenseAddItem POST /api/oa/expense/item/add/:id 新增费用明细(仅草稿)。
func (c *AppOaController) ExpenseAddItem() {
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.expenseReadItemPayload()
if !ok {
return
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
item, code, msg := reimburseAddItemCore(claims, id, payload, scheme, host)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(item)
}
// ExpenseUpdateItem POST /api/oa/expense/item/update/:id/:item_id 更新费用明细(仅草稿)。
func (c *AppOaController) ExpenseUpdateItem() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
itemID := c.expenseItemIDParam()
if id == 0 || itemID == 0 {
c.oaErr(400, 400, "无效ID")
return
}
payload, ok := c.expenseReadItemPayload()
if !ok {
return
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
item, code, msg := reimburseUpdateItemCore(claims, id, itemID, payload, scheme, host)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(item)
}
// ExpenseDeleteItem DELETE /api/oa/expense/item/delete/:id/:item_id 删除费用明细(仅草稿)。
func (c *AppOaController) ExpenseDeleteItem() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
itemID := c.expenseItemIDParam()
if id == 0 || itemID == 0 {
c.oaErr(400, 400, "无效ID")
return
}
code, msg := reimburseDeleteItemCore(claims, id, itemID)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(nil)
}
// ExpenseSubmit POST /api/oa/expense/submit/:id 提交审批(仅草稿,需已配置审批流)。
func (c *AppOaController) ExpenseSubmit() {
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 := reimburseSubmitCore(claims, id)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(nil)
}
// ExpenseStatus POST /api/oa/expense/status/:id 手动确认更改单据状态(body: {status})。
func (c *AppOaController) ExpenseStatus() {
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 reimbursementStatusPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if len(raw) > 0 && (err != nil || json.Unmarshal(raw, &payload) != nil) {
c.oaErr(400, 400, "参数错误")
return
}
newStatus, code, msg := reimburseUpdateStatusCore(claims, id, payload.Status)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(map[string]interface{}{"status": newStatus})
}
// ExpenseRecords GET /api/oa/expense/records/:id 审批记录时间线。
func (c *AppOaController) ExpenseRecords() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
rows, code, msg := reimburseRecordsData(claims, id)
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(rows)
}
// ExpenseTypes GET /api/oa/expense/types 费用类型下拉(全局通用 + 本租户专有,仅启用)。
func (c *AppOaController) ExpenseTypes() {
claims, err := c.oaClaims()
if err != nil {
c.oaErr(401, 401, "未登录或无权限")
return
}
rows, err := queryReimburseExpenseTypes(claims.TenantId, true)
if err != nil {
c.oaErr(500, 500, "查询费用类型失败")
return
}
list := make([]map[string]interface{}, 0, len(rows))
for _, row := range rows {
list = append(list, expenseTypeDTO(row, claims.TenantId))
}
c.oaOk(list)
}
// ExpenseUploadImage POST /api/oa/expense/upload(multipart,字段名 file)
// 上传报销发票图片,共享 uploadTenantImage,返回 {id,url,name};
// 随后由费用明细接口把文件 ID 写进 receipt_file_ids。
func (c *AppOaController) ExpenseUploadImage() {
c.uploadTenantImage()
}
// expenseItemIDParam 解析 :item_id 路由参数,非法返回 0。
func (c *AppOaController) expenseItemIDParam() uint64 {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":item_id"), 10, 64)
if err != nil {
return 0
}
return id
}
+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})
}
+125 -62
View File
@@ -160,6 +160,21 @@ func (c *BackendOaScheduleController) List() {
endDate := strings.TrimSpace(c.GetString("end_date"))
category := strings.TrimSpace(c.GetString("category"))
data, code, msg := oaScheduleListData(claims.TenantId, claims.UserID,
page, pageSize, keyword, status, priority, startDate, endDate, category)
if msg != "" {
c.oasJsonErr(code, code, msg)
return
}
c.oasOk(data)
}
// oaScheduleListData 日程列表的共享查询逻辑,Web 后台(/backend/oa/schedule/list)
// 与移动端(/api/oa/schedule/list)共用。
// 返回 (数据, HTTP 状态码, 错误信息),错误信息为空串表示成功。
func oaScheduleListData(tid, userID, page, pageSize int,
keyword, status, priority, startDate, endDate, category string) (map[string]interface{}, int, string) {
if page < 1 {
page = 1
}
@@ -181,7 +196,7 @@ func (c *BackendOaScheduleController) List() {
// 分类角标按全量统计(只受关键词/优先级影响),不随日历翻月或区间筛选跳动
countOf := func(f oaScheduleFilter) int64 {
n, _ := oaScheduleQS(claims.TenantId, claims.UserID, f).Count()
n, _ := oaScheduleQS(tid, userID, f).Count()
return n
}
today := time.Now().Format("2006-01-02")
@@ -214,16 +229,15 @@ func (c *BackendOaScheduleController) List() {
listFilter.Status = 1
}
qs := oaScheduleQS(claims.TenantId, claims.UserID, listFilter)
qs := oaScheduleQS(tid, userID, listFilter)
total, _ := qs.Count()
var list []models.OaSchedule
_, err = qs.OrderBy("schedule_date", "start_time", "-id").
_, err := qs.OrderBy("schedule_date", "start_time", "-id").
Limit(pageSize).Offset((page - 1) * pageSize).
All(&list)
if err != nil && err != orm.ErrNoRows {
c.oasJsonErr(500, 500, "查询失败")
return
return nil, 500, "查询失败"
}
if list == nil {
list = []models.OaSchedule{}
@@ -234,11 +248,11 @@ func (c *BackendOaScheduleController) List() {
list[i].ScheduleDate = normalizeOaDate(list[i].ScheduleDate)
}
c.oasOk(map[string]interface{}{
return map[string]interface{}{
"list": list,
"total": total,
"counts": counts,
})
}, 200, ""
}
// Holidays GET /backend/oa/holiday
@@ -590,25 +604,36 @@ func parseOaScheduleImages(raw string) []oaScheduleImage {
// parseOaSchedulePayload 读取并校验新建/编辑日程的请求体;校验失败时直接输出错误响应。
func (c *BackendOaScheduleController) parseOaSchedulePayload() (oaSchedulePayload, bool) {
var payload oaSchedulePayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &payload) != nil {
if err != nil {
c.oasJsonErr(400, 400, "参数错误")
return oaSchedulePayload{}, false
}
payload, msg := parseOaSchedulePayloadBody(raw)
if msg != "" {
c.oasJsonErr(400, 400, msg)
return payload, false
}
return payload, true
}
// parseOaSchedulePayloadBody 新建/编辑日程请求体的共享校验逻辑,
// Web 后台与移动端共用。返回 (校验后的载荷, 错误信息),错误信息为空串表示成功。
func parseOaSchedulePayloadBody(raw []byte) (oaSchedulePayload, string) {
var payload oaSchedulePayload
if len(raw) == 0 || json.Unmarshal(raw, &payload) != nil {
return payload, "参数错误"
}
payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" {
c.oasJsonErr(400, 400, "请输入日程标题")
return payload, false
return payload, "请输入日程标题"
}
if utf8.RuneCountInString(payload.Title) > 80 {
c.oasJsonErr(400, 400, "日程标题不能超过80字")
return payload, false
return payload, "日程标题不能超过80字"
}
if !isValidOaDate(payload.ScheduleDate) {
c.oasJsonErr(400, 400, "日程日期无效")
return payload, false
return payload, "日程日期无效"
}
if payload.AllDay != 0 {
payload.AllDay = 1
@@ -616,17 +641,14 @@ func (c *BackendOaScheduleController) parseOaSchedulePayload() (oaSchedulePayloa
payload.EndTime = ""
} else {
if !isValidOaTime(payload.StartTime) {
c.oasJsonErr(400, 400, "开始时间无效")
return payload, false
return payload, "开始时间无效"
}
if payload.EndTime != "" {
if !isValidOaTime(payload.EndTime) {
c.oasJsonErr(400, 400, "结束时间无效")
return payload, false
return payload, "结束时间无效"
}
if payload.EndTime < payload.StartTime {
c.oasJsonErr(400, 400, "结束时间不能早于开始时间")
return payload, false
return payload, "结束时间不能早于开始时间"
}
}
}
@@ -649,18 +671,16 @@ func (c *BackendOaScheduleController) parseOaSchedulePayload() (oaSchedulePayloa
}
payload.RemindChannels = validChannels
if len(validChannels) == 0 {
c.oasJsonErr(400, 400, "请至少选择一个提醒渠道")
return payload, false
return payload, "请至少选择一个提醒渠道"
}
if _, err := time.ParseInLocation("2006-01-02 15:04:05", payload.ReminderTime, time.Local); err != nil {
c.oasJsonErr(400, 400, "提醒时间格式不合法,支持 YYYY-MM-DD HH:mm:ss")
return payload, false
return payload, "提醒时间格式不合法,支持 YYYY-MM-DD HH:mm:ss"
}
if payload.AdvanceMinutes < 0 {
payload.AdvanceMinutes = 0
}
}
return payload, true
return payload, ""
}
// Create POST /backend/oa/schedule/create
@@ -676,6 +696,17 @@ func (c *BackendOaScheduleController) Create() {
return
}
item, code, msg := oaScheduleCreateCore(claims, payload)
if msg != "" {
c.oasJsonErr(code, code, msg)
return
}
c.oasOk(item)
}
// oaScheduleCreateCore 创建日程(含提醒绑定)的共享逻辑,Web 后台与移动端共用。
// 返回 (新建日程, HTTP 状态码, 错误信息),错误信息为空串表示成功。
func oaScheduleCreateCore(claims *jwtutil.Claims, payload oaSchedulePayload) (*models.OaSchedule, int, string) {
now := time.Now()
item := &models.OaSchedule{
Tid: claims.TenantId,
@@ -695,8 +726,7 @@ func (c *BackendOaScheduleController) Create() {
UpdateTime: &now,
}
if _, err := models.Orm.Insert(item); err != nil {
c.oasJsonErr(500, 500, "保存失败")
return
return nil, 500, "保存失败"
}
// 开启提醒:生成绑定的提醒任务,并把其 ID 回写到日程。
@@ -713,17 +743,15 @@ func (c *BackendOaScheduleController) Create() {
if rerr != nil {
// 回滚已创建的日程,避免产生孤立日程
_, _ = models.Orm.QueryTable(new(models.OaSchedule)).Filter("id", item.ID).Delete()
c.oasJsonErr(500, 500, rerr.Error())
return
return nil, 500, rerr.Error()
}
item.ReminderID = rid
if _, err := models.Orm.Update(item, "ReminderID"); err != nil {
c.oasJsonErr(500, 500, "保存失败")
return
return nil, 500, "保存失败"
}
}
c.oasOk(item)
return item, 200, ""
}
// Update POST /backend/oa/schedule/update/:id
@@ -745,16 +773,26 @@ func (c *BackendOaScheduleController) Update() {
return
}
item, code, msg := oaScheduleUpdateCore(claims, id, payload)
if msg != "" {
c.oasJsonErr(code, code, msg)
return
}
c.oasOk(item)
}
// oaScheduleUpdateCore 更新日程(含提醒重绑)的共享逻辑,Web 后台与移动端共用。
// 返回 (更新后日程, HTTP 状态码, 错误信息),错误信息为空串表示成功。
func oaScheduleUpdateCore(claims *jwtutil.Claims, id uint64, payload oaSchedulePayload) (*models.OaSchedule, int, string) {
var item models.OaSchedule
err = models.Orm.QueryTable(new(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.oasJsonErr(404, 404, "日程不存在")
return
return nil, 404, "日程不存在"
}
now := time.Now()
@@ -781,8 +819,7 @@ func (c *BackendOaScheduleController) Update() {
payload.AdvanceMinutes,
)
if rerr != nil {
c.oasJsonErr(500, 500, rerr.Error())
return
return nil, 500, rerr.Error()
}
if item.ReminderID > 0 {
deleteBoundReminder(item.ReminderID)
@@ -796,11 +833,10 @@ func (c *BackendOaScheduleController) Update() {
if _, err := models.Orm.Update(&item,
"Title", "Content", "ScheduleDate", "StartTime", "EndTime",
"AllDay", "Color", "Priority", "ReminderID", "UpdateTime"); err != nil {
c.oasJsonErr(500, 500, "保存失败")
return
return nil, 500, "保存失败"
}
c.oasOk(item)
return &item, 200, ""
}
// SetImages POST /backend/oa/schedule/images/:id
@@ -827,33 +863,42 @@ func (c *BackendOaScheduleController) SetImages() {
c.oasJsonErr(400, 400, "参数错误")
return
}
imagesJSON, ok := normalizeOaScheduleImages(payload.Images)
if !ok {
c.oasJsonErr(400, 400, "图片数量过多")
images, code, msg := oaScheduleSetImagesCore(claims, id, payload.Images)
if msg != "" {
c.oasJsonErr(code, code, msg)
return
}
c.oasOk(map[string]interface{}{"images": images})
}
// oaScheduleSetImagesCore 保存日程相关图片(只存文件引用)的共享逻辑,
// Web 后台与移动端共用。返回 (图片列表, HTTP 状态码, 错误信息),错误信息为空串表示成功。
func oaScheduleSetImagesCore(claims *jwtutil.Claims, id uint64, images []oaScheduleImage) ([]oaScheduleImage, int, string) {
imagesJSON, ok := normalizeOaScheduleImages(images)
if !ok {
return nil, 400, "图片数量过多"
}
var item models.OaSchedule
err = models.Orm.QueryTable(new(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.oasJsonErr(404, 404, "日程不存在")
return
return nil, 404, "日程不存在"
}
now := time.Now()
item.Images = imagesJSON
item.UpdateTime = &now
if _, err := models.Orm.Update(&item, "Images", "UpdateTime"); err != nil {
c.oasJsonErr(500, 500, "保存失败")
return
return nil, 500, "保存失败"
}
c.oasOk(map[string]interface{}{"images": parseOaScheduleImages(imagesJSON)})
return parseOaScheduleImages(imagesJSON), 200, ""
}
// UpdateContent POST /backend/oa/schedule/content/:id 仅更新备注内容。
@@ -918,17 +963,27 @@ func (c *BackendOaScheduleController) Delete() {
return
}
code, msg := oaScheduleDeleteCore(claims, id)
if msg != "" {
c.oasJsonErr(code, code, msg)
return
}
c.oasOk(nil)
}
// oaScheduleDeleteCore 软删除日程并清理绑定提醒的共享逻辑,Web 后台与移动端共用。
// 返回 (HTTP 状态码, 错误信息),错误信息为空串表示成功。
func oaScheduleDeleteCore(claims *jwtutil.Claims, id uint64) (int, string) {
now := time.Now()
var item models.OaSchedule
err = models.Orm.QueryTable(new(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.oasJsonErr(404, 404, "日程不存在或已删除")
return
return 404, "日程不存在或已删除"
}
// 先删除绑定的提醒任务,避免提醒工具里残留孤立任务
@@ -940,11 +995,10 @@ func (c *BackendOaScheduleController) Delete() {
item.DeleteTime = &now
item.UpdateTime = &now
if _, err := models.Orm.Update(&item, "IsDeleted", "DeleteTime", "UpdateTime"); err != nil {
c.oasJsonErr(500, 500, "删除失败")
return
return 500, "删除失败"
}
c.oasOk(nil)
return 200, ""
}
// FinishSchedule POST /backend/oa/schedule/finish/:id
@@ -964,16 +1018,26 @@ func (c *BackendOaScheduleController) FinishSchedule() {
return
}
newStatus, code, msg := oaScheduleToggleCore(claims, id)
if msg != "" {
c.oasJsonErr(code, code, msg)
return
}
c.oasOk(map[string]interface{}{"status": newStatus})
}
// oaScheduleToggleCore 切换日程完成状态的共享逻辑,Web 后台与移动端共用。
// 返回 (新状态, HTTP 状态码, 错误信息),错误信息为空串表示成功。
func oaScheduleToggleCore(claims *jwtutil.Claims, id uint64) (int8, int, string) {
var item models.OaSchedule
err = models.Orm.QueryTable(new(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.oasJsonErr(404, 404, "日程不存在")
return
return 0, 404, "日程不存在"
}
now := time.Now()
@@ -990,11 +1054,10 @@ func (c *BackendOaScheduleController) FinishSchedule() {
item.UpdateTime = &now
if _, err := models.Orm.Update(&item, "Status", "FinishTime", "UpdateTime"); err != nil {
c.oasJsonErr(500, 500, "操作失败")
return
return 0, 500, "操作失败"
}
c.oasOk(map[string]interface{}{"status": newStatus})
return newStatus, 200, ""
}
// nextOaDate 返回 dateStr 之后的第 days 天,格式仍为 "YYYY-MM-DD"。
+227 -130
View File
@@ -24,17 +24,17 @@ type BackendReimburseController struct {
}
type reimbursementItemPayload struct {
ID uint64 `json:"id"`
ExpenseType string `json:"expense_type"`
ExpenseDate string `json:"expense_date"`
Amount float64 `json:"amount"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
Description string `json:"description"`
ReceiptFlag int8 `json:"receipt_flag"`
ReceiptFileIDs string `json:"receipt_file_ids"`
ReceiptURLs string `json:"receipt_urls"`
ReceiptAmounts *string `json:"receipt_amounts,omitempty"`
ID uint64 `json:"id"`
ExpenseType string `json:"expense_type"`
ExpenseDate string `json:"expense_date"`
Amount float64 `json:"amount"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
Description string `json:"description"`
ReceiptFlag int8 `json:"receipt_flag"`
ReceiptFileIDs string `json:"receipt_file_ids"`
ReceiptURLs string `json:"receipt_urls"`
ReceiptAmounts *string `json:"receipt_amounts,omitempty"`
}
type reimbursementPayload struct {
@@ -88,7 +88,9 @@ func (c *BackendReimburseController) getID() (uint64, bool) {
return id, err == nil && id > 0
}
func (c *BackendReimburseController) findOwned(id uint64, claims *jwtutil.Claims) (*models.BackendReimbursement, error) {
// findOwnedReimbursement 按 id + 租户 + 申请人 查询本人报销单(未删除)。
// 报销单为个人单据:与列表口径一致,按 JWT 中 tid + user_id 双重隔离。
func findOwnedReimbursement(claims *jwtutil.Claims, id uint64) (*models.BackendReimbursement, error) {
row := &models.BackendReimbursement{}
err := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("id", id).
@@ -301,7 +303,7 @@ func reimbursementItemDTO(item models.BackendReimbursementItem, scheme, host str
"amount": item.Amount, "quantity": item.Quantity, "unit_price": item.UnitPrice,
"description": item.Description, "receipt_flag": receiptFlag,
"receipt_file_ids": item.ReceiptFileIDs,
"receipt_urls": receiptURLs, "receipt_amounts": item.ReceiptAmounts,
"receipt_urls": receiptURLs, "receipt_amounts": item.ReceiptAmounts,
"receipt_files": receiptFiles,
}
}
@@ -310,7 +312,7 @@ func reimbursementDTO(row models.BackendReimbursement, items []models.BackendRei
_, userName := services.OperationLogUser(&row.Tid, row.UserID)
result := map[string]interface{}{
"id": row.ID, "tid": row.Tid, "uid": row.UID, "user_id": row.UserID,
"user_name": userName,
"user_name": userName,
"department_id": row.DepartmentID, "total_amount": row.TotalAmount,
"status": row.Status, "apply_date": row.ApplyDate.Format("2006-01-02"),
"description": row.Description, "current_approver_id": row.CurrentApproverID,
@@ -333,16 +335,26 @@ func (c *BackendReimburseController) List() {
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
keyword := strings.TrimSpace(c.GetString("keyword"))
status, statusErr := c.GetInt("status")
data, code, msg := reimburseListData(claims, page, pageSize, keyword, statusErr == nil, status)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", data)
}
// reimburseListData 报销单列表共享逻辑:Web 后台与移动端(/api/oa/expense/list)共用。
// 返回 (数据, HTTP 状态码, 错误信息),错误信息为空串表示成功。
func reimburseListData(claims *jwtutil.Claims, page, pageSize int, keyword string, hasStatus bool, status int) (map[string]interface{}, int, string) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
keyword := strings.TrimSpace(c.GetString("keyword"))
status, statusErr := c.GetInt("status")
hasStatus := statusErr == nil
qs := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("tid", claims.TenantId).Filter("user_id", claims.UserID).Filter("is_deleted", 0)
if keyword != "" {
@@ -353,14 +365,12 @@ func (c *BackendReimburseController) List() {
}
total, err := qs.Count()
if err != nil {
c.reply(500, "查询失败", nil)
return
return nil, 500, "查询失败"
}
var rows []models.BackendReimbursement
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询失败", nil)
return
return nil, 500, "查询失败"
}
if rows == nil {
rows = []models.BackendReimbursement{}
@@ -383,7 +393,7 @@ func (c *BackendReimburseController) List() {
"update_time": row.UpdateTime,
})
}
c.reply(200, "success", map[string]interface{}{"list": list, "total": total})
return map[string]interface{}{"list": list, "total": total}, 200, ""
}
// Detail GET /backend/reimbursements/:id
@@ -398,21 +408,29 @@ func (c *BackendReimburseController) Detail() {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
scheme, host := services.PublicRequestBaseURL(&c.Controller)
data, code, msg := reimburseDetailData(claims, id, scheme, host)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", data)
}
// reimburseDetailData 报销单详情共享逻辑(主单 + 明细 + 发票文件)。
func reimburseDetailData(claims *jwtutil.Claims, id uint64, scheme, host string) (map[string]interface{}, int, string) {
row, err := findOwnedReimbursement(claims, id)
if err != nil {
return nil, 404, "报销单不存在"
}
var items []models.BackendReimbursementItem
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("reimbursement_id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).
OrderBy("id").All(&items)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询明细失败: "+err.Error(), nil)
return
return nil, 500, "查询明细失败: " + err.Error()
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
c.reply(200, "success", reimbursementDTO(*row, items, scheme, host))
return reimbursementDTO(*row, items, scheme, host), 200, ""
}
// Create POST /backend/reimbursements
@@ -427,10 +445,19 @@ func (c *BackendReimburseController) Create() {
c.reply(400, "参数错误", nil)
return
}
id, code, msg := reimburseCreateCore(claims, payload)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", map[string]interface{}{"id": id})
}
// reimburseCreateCore 创建报销单(草稿)共享逻辑。
func reimburseCreateCore(claims *jwtutil.Claims, payload reimbursementPayload) (uint64, int, string) {
applyDate, err := parseReimburseDate(payload.ApplyDate)
if err != nil {
c.reply(400, "申请日期格式错误", nil)
return
return 0, 400, "申请日期格式错误"
}
uid := uint64(claims.UserID)
row := &models.BackendReimbursement{
@@ -439,10 +466,9 @@ func (c *BackendReimburseController) Create() {
Description: nullableReimburseString(payload.Description),
}
if err := saveReimbursement(row); err != nil {
c.reply(500, "创建报销单失败", nil)
return
return 0, 500, "创建报销单失败"
}
c.reply(200, "success", map[string]interface{}{"id": row.ID})
return row.ID, 200, ""
}
// Update POST /backend/reimbursements/:id
@@ -457,31 +483,37 @@ func (c *BackendReimburseController) Update() {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
return
}
if row.Status != 0 {
c.reply(400, "只有草稿可以编辑", nil)
return
}
payload, err := c.parsePayload()
if err != nil {
c.reply(400, "参数错误", nil)
return
}
code, msg := reimburseUpdateCore(claims, id, payload)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", map[string]interface{}{"id": id})
}
// reimburseUpdateCore 编辑报销单(仅草稿)共享逻辑。
func reimburseUpdateCore(claims *jwtutil.Claims, id uint64, payload reimbursementPayload) (int, string) {
row, err := findOwnedReimbursement(claims, id)
if err != nil {
return 404, "报销单不存在"
}
if row.Status != 0 {
return 400, "只有草稿可以编辑"
}
applyDate, err := parseReimburseDate(payload.ApplyDate)
if err != nil {
c.reply(400, "申请日期格式错误", nil)
return
return 400, "申请日期格式错误"
}
row.ApplyDate, row.Description = applyDate, nullableReimburseString(payload.Description)
if err := saveReimbursement(row); err != nil {
c.reply(500, "更新报销单失败", nil)
return
return 500, "更新报销单失败"
}
c.reply(200, "success", map[string]interface{}{"id": row.ID})
return 200, ""
}
// Delete DELETE /backend/reimbursements/:id
@@ -496,25 +528,32 @@ func (c *BackendReimburseController) Delete() {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
code, msg := reimburseDeleteCore(claims, id)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", nil)
}
// reimburseDeleteCore 删除报销单(仅草稿,连明细一起软删)共享逻辑。
func reimburseDeleteCore(claims *jwtutil.Claims, id uint64) (int, string) {
row, err := findOwnedReimbursement(claims, id)
if err != nil {
return 404, "报销单不存在"
}
if row.Status != 0 {
c.reply(400, "只有草稿可以删除", nil)
return
return 400, "只有草稿可以删除"
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
if err != nil {
c.reply(500, "删除失败", nil)
return
return 500, "删除失败"
}
_, _ = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("reimbursement_id", id).Update(orm.Params{"is_deleted": 1, "delete_time": now})
c.reply(200, "success", nil)
return 200, ""
}
func (c *BackendReimburseController) parseItemPayload() (reimbursementItemPayload, error) {
@@ -532,7 +571,8 @@ func (c *BackendReimburseController) getItemID() (uint64, bool) {
return id, err == nil && id > 0
}
func (c *BackendReimburseController) findOwnedItem(id, reimbursementID uint64, claims *jwtutil.Claims) (*models.BackendReimbursementItem, error) {
// findOwnedReimbursementItem 查询本人报销单下的费用明细(包级共享,移动端复用)。
func findOwnedReimbursementItem(claims *jwtutil.Claims, id, reimbursementID uint64) (*models.BackendReimbursementItem, error) {
item := &models.BackendReimbursementItem{}
err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("id", id).
@@ -558,17 +598,17 @@ func validateReimbursementItem(item reimbursementItemPayload) (time.Time, error)
return date, nil
}
func (c *BackendReimburseController) ensureDraftReimbursement(id uint64, claims *jwtutil.Claims) (*models.BackendReimbursement, bool) {
row, err := c.findOwned(id, claims)
// ensureDraftReimbursement 校验报销单存在且为草稿(包级共享,移动端复用)。
// 返回 (报销单, HTTP 状态码, 错误信息),错误信息为空串表示通过。
func ensureDraftReimbursement(claims *jwtutil.Claims, id uint64) (*models.BackendReimbursement, int, string) {
row, err := findOwnedReimbursement(claims, id)
if err != nil {
c.reply(404, "报销单不存在", nil)
return nil, false
return nil, 404, "报销单不存在"
}
if row.Status != 0 {
c.reply(400, "只有草稿可以维护费用明细", nil)
return nil, false
return nil, 400, "只有草稿可以维护费用明细"
}
return row, true
return row, 200, ""
}
func recalculateReimbursementTotal(id uint64) error {
@@ -602,25 +642,38 @@ func (c *BackendReimburseController) AddItem() {
c.reply(400, "无效报销单ID", nil)
return
}
if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok {
return
}
payload, err := c.parseItemPayload()
if err != nil {
c.reply(400, "参数错误", nil)
return
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
item, code, msg := reimburseAddItemCore(claims, reimbursementID, payload, scheme, host)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", item)
}
// reimburseAddItemCore 新增费用明细共享逻辑(仅草稿;自动重算报销总额)。
func reimburseAddItemCore(claims *jwtutil.Claims, reimbursementID uint64, payload reimbursementItemPayload, scheme, host string) (map[string]interface{}, int, string) {
if _, _, msg := ensureDraftReimbursement(claims, reimbursementID); msg != "" {
code := 400
if msg == "报销单不存在" {
code = 404
}
return nil, code, msg
}
date, err := validateReimbursementItem(payload)
if err != nil {
c.reply(400, err.Error(), nil)
return
return nil, 400, err.Error()
}
uid := uint64(claims.UserID)
quantity := payload.Quantity
if quantity <= 0 {
quantity = 1
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
item := &models.BackendReimbursementItem{
Tid: uint64(claims.TenantId), UID: &uid, ReimbursementID: reimbursementID,
ExpenseType: strings.TrimSpace(payload.ExpenseType), ExpenseDate: date,
@@ -629,14 +682,12 @@ func (c *BackendReimburseController) AddItem() {
}
applyReceiptToItem(item, payload, scheme, host)
if _, err := models.Orm.Insert(item); err != nil {
c.reply(500, "新增费用明细失败", nil)
return
return nil, 500, "新增费用明细失败"
}
if err := recalculateReimbursementTotal(reimbursementID); err != nil {
c.reply(500, "更新报销总额失败", nil)
return
return nil, 500, "更新报销总额失败"
}
c.reply(200, "success", reimbursementItemDTO(*item, scheme, host))
return reimbursementItemDTO(*item, scheme, host), 200, ""
}
// UpdateItem POST /backend/reimbursements/:id/items/:item_id
@@ -656,29 +707,41 @@ func (c *BackendReimburseController) UpdateItem() {
c.reply(400, "无效费用明细ID", nil)
return
}
if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok {
return
}
item, err := c.findOwnedItem(itemID, reimbursementID, claims)
if err != nil {
c.reply(404, "费用明细不存在", nil)
return
}
payload, err := c.parseItemPayload()
if err != nil {
c.reply(400, "参数错误", nil)
return
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
item, code, msg := reimburseUpdateItemCore(claims, reimbursementID, itemID, payload, scheme, host)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", item)
}
// reimburseUpdateItemCore 更新费用明细共享逻辑(仅草稿;自动重算报销总额)。
func reimburseUpdateItemCore(claims *jwtutil.Claims, reimbursementID, itemID uint64, payload reimbursementItemPayload, scheme, host string) (map[string]interface{}, int, string) {
if _, _, msg := ensureDraftReimbursement(claims, reimbursementID); msg != "" {
code := 400
if msg == "报销单不存在" {
code = 404
}
return nil, code, msg
}
item, err := findOwnedReimbursementItem(claims, itemID, reimbursementID)
if err != nil {
return nil, 404, "费用明细不存在"
}
date, err := validateReimbursementItem(payload)
if err != nil {
c.reply(400, err.Error(), nil)
return
return nil, 400, err.Error()
}
quantity := payload.Quantity
if quantity <= 0 {
quantity = 1
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
item.ExpenseType = strings.TrimSpace(payload.ExpenseType)
item.ExpenseDate = date
item.Amount = payload.Amount
@@ -687,14 +750,12 @@ func (c *BackendReimburseController) UpdateItem() {
item.Description = strings.TrimSpace(payload.Description)
applyReceiptToItem(item, payload, scheme, host)
if _, err := models.Orm.Update(item); err != nil {
c.reply(500, "更新费用明细失败", nil)
return
return nil, 500, "更新费用明细失败"
}
if err := recalculateReimbursementTotal(reimbursementID); err != nil {
c.reply(500, "更新报销总额失败", nil)
return
return nil, 500, "更新报销总额失败"
}
c.reply(200, "success", reimbursementItemDTO(*item, scheme, host))
return reimbursementItemDTO(*item, scheme, host), 200, ""
}
// DeleteItem DELETE /backend/reimbursements/:id/items/:item_id
@@ -714,26 +775,36 @@ func (c *BackendReimburseController) DeleteItem() {
c.reply(400, "无效费用明细ID", nil)
return
}
if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok {
code, msg := reimburseDeleteItemCore(claims, reimbursementID, itemID)
if msg != "" {
c.reply(code, msg, nil)
return
}
item, err := c.findOwnedItem(itemID, reimbursementID, claims)
c.reply(200, "success", nil)
}
// reimburseDeleteItemCore 删除费用明细共享逻辑(仅草稿;自动重算报销总额)。
func reimburseDeleteItemCore(claims *jwtutil.Claims, reimbursementID, itemID uint64) (int, string) {
if _, _, msg := ensureDraftReimbursement(claims, reimbursementID); msg != "" {
if msg == "报销单不存在" {
return 404, msg
}
return 400, msg
}
item, err := findOwnedReimbursementItem(claims, itemID, reimbursementID)
if err != nil {
c.reply(404, "费用明细不存在", nil)
return
return 404, "费用明细不存在"
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("id", item.ID).Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
if err != nil {
c.reply(500, "删除费用明细失败", nil)
return
return 500, "删除费用明细失败"
}
if err := recalculateReimbursementTotal(reimbursementID); err != nil {
c.reply(500, "更新报销总额失败", nil)
return
return 500, "更新报销总额失败"
}
c.reply(200, "success", nil)
return 200, ""
}
// Submit POST /backend/reimbursements/:id/submit
@@ -748,30 +819,37 @@ func (c *BackendReimburseController) Submit() {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
code, msg := reimburseSubmitCore(claims, id)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", nil)
}
// reimburseSubmitCore 提交审批共享逻辑:仅草稿可提交,需租户已配置启用审批流,
// 提交后状态置为审批中(1),并写入 action=1 的审批记录。
func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
row, err := findOwnedReimbursement(claims, id)
if err != nil {
return 404, "报销单不存在"
}
if row.Status != 0 {
c.reply(400, "当前状态不可提交", nil)
return
return 400, "当前状态不可提交"
}
var flow models.BackendApprovalFlow
err = models.Orm.QueryTable(new(models.BackendApprovalFlow)).
Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0).
OrderBy("step_order").One(&flow)
if err != nil {
c.reply(400, "尚未配置审批流程", nil)
return
return 400, "尚未配置审批流程"
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
"status": 1, "current_step": flow.StepOrder, "current_approver_id": 0, "update_time": now,
})
if err != nil {
c.reply(500, "提交失败", nil)
return
return 500, "提交失败"
}
_, err = models.Orm.Insert(&models.BackendApprovalRecord{
Tid: uint64(claims.TenantId), UID: uint64Ptr(uint64(claims.UserID)), ReimbursementID: id,
@@ -779,10 +857,9 @@ func (c *BackendReimburseController) Submit() {
Step: flow.StepOrder, PrevStep: 0,
})
if err != nil {
c.reply(500, "提交审批记录失败", nil)
return
return 500, "提交审批记录失败"
}
c.reply(200, "success", nil)
return 200, ""
}
type reimbursementStatusPayload struct {
@@ -820,28 +897,35 @@ func (c *BackendReimburseController) UpdateStatus() {
c.reply(400, "参数错误", nil)
return
}
if !manualConfirmStatuses[payload.Status] {
c.reply(400, "不支持的状态值", nil)
newStatus, code, msg := reimburseUpdateStatusCore(claims, id, payload.Status)
if msg != "" {
c.reply(code, msg, nil)
return
}
row, err := c.findOwned(id, claims)
c.reply(200, "success", map[string]interface{}{"status": newStatus})
}
// reimburseUpdateStatusCore 手动确认更改单据状态共享逻辑。
// 返回 (生效状态, HTTP 状态码, 错误信息)。
func reimburseUpdateStatusCore(claims *jwtutil.Claims, id uint64, status int8) (int8, int, string) {
if !manualConfirmStatuses[status] {
return 0, 400, "不支持的状态值"
}
row, err := findOwnedReimbursement(claims, id)
if err != nil {
c.reply(404, "报销单不存在", nil)
return
return 0, 404, "报销单不存在"
}
if row.Status == payload.Status {
c.reply(200, "success", map[string]interface{}{"status": row.Status})
return
if row.Status == status {
return row.Status, 200, ""
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
"status": payload.Status, "update_time": now,
"status": status, "update_time": now,
})
if err != nil {
c.reply(500, "更新状态失败", nil)
return
return 0, 500, "更新状态失败"
}
c.reply(200, "success", map[string]interface{}{"status": payload.Status})
return status, 200, ""
}
// Records GET /backend/reimbursements/:id/records
@@ -856,22 +940,30 @@ func (c *BackendReimburseController) Records() {
c.reply(400, "无效ID", nil)
return
}
if _, err := c.findOwned(id, claims); err != nil {
c.reply(404, "报销单不存在", nil)
rows, code, msg := reimburseRecordsData(claims, id)
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, "success", rows)
}
// reimburseRecordsData 审批记录共享逻辑(按 step、id 升序)。
func reimburseRecordsData(claims *jwtutil.Claims, id uint64) ([]models.BackendApprovalRecord, int, string) {
if _, err := findOwnedReimbursement(claims, id); err != nil {
return nil, 404, "报销单不存在"
}
var rows []models.BackendApprovalRecord
_, err := models.Orm.QueryTable(new(models.BackendApprovalRecord)).
Filter("tid", claims.TenantId).Filter("reimbursement_id", id).Filter("is_deleted", 0).
OrderBy("step", "id").All(&rows)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询审批记录失败", nil)
return
return nil, 500, "查询审批记录失败"
}
if rows == nil {
rows = []models.BackendApprovalRecord{}
}
c.reply(200, "success", rows)
return rows, 200, ""
}
type expenseTypePayload struct {
@@ -896,6 +988,11 @@ func expenseTypeDTO(row models.BackendReimbursementExpenseType, tenantID int) ma
}
func (c *BackendReimburseController) queryExpenseTypes(tenantID int, activeOnly bool) ([]models.BackendReimbursementExpenseType, error) {
return queryReimburseExpenseTypes(tenantID, activeOnly)
}
// queryReimburseExpenseTypes 费用类型查询共享逻辑:全局通用(tid=0)+ 当前租户专有。
func queryReimburseExpenseTypes(tenantID int, activeOnly bool) ([]models.BackendReimbursementExpenseType, error) {
qs := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
SetCond(expenseTypeScopeCond(tenantID)).
Filter("is_deleted", 0)
+25
View File
@@ -96,6 +96,31 @@ func Register() {
// OA 工作台仪表盘(移动端首屏顶部聚合:待办/报销统计 + 通知公告 + 今日日程)
beego.Router("/api/oa/dashboard", &controllers.AppOaController{}, "get:Dashboard")
// OA 工作日程(日历待办,与 /backend/oa/schedule/* 同一套数据;小程序「工作日程」页面使用)
beego.Router("/api/oa/schedule/list", &controllers.AppOaController{}, "get:ScheduleList")
beego.Router("/api/oa/schedule/detail/:id", &controllers.AppOaController{}, "get:ScheduleDetail")
beego.Router("/api/oa/schedule/create", &controllers.AppOaController{}, "post:ScheduleCreate")
beego.Router("/api/oa/schedule/update/:id", &controllers.AppOaController{}, "post:ScheduleUpdate")
beego.Router("/api/oa/schedule/delete/:id", &controllers.AppOaController{}, "delete:ScheduleDelete")
beego.Router("/api/oa/schedule/finish/:id", &controllers.AppOaController{}, "post:ScheduleFinish")
beego.Router("/api/oa/schedule/upload", &controllers.AppOaController{}, "post:ScheduleUploadImage")
beego.Router("/api/oa/schedule/images/:id", &controllers.AppOaController{}, "post:ScheduleImages")
// OA 报销管理(与 /backend/reimbursements* 同一套数据;小程序「报销管理」页面使用)
beego.Router("/api/oa/expense/list", &controllers.AppOaController{}, "get:ExpenseList")
beego.Router("/api/oa/expense/types", &controllers.AppOaController{}, "get:ExpenseTypes")
beego.Router("/api/oa/expense/upload", &controllers.AppOaController{}, "post:ExpenseUploadImage")
beego.Router("/api/oa/expense/detail/:id", &controllers.AppOaController{}, "get:ExpenseDetail")
beego.Router("/api/oa/expense/create", &controllers.AppOaController{}, "post:ExpenseCreate")
beego.Router("/api/oa/expense/update/:id", &controllers.AppOaController{}, "post:ExpenseUpdate")
beego.Router("/api/oa/expense/delete/:id", &controllers.AppOaController{}, "delete:ExpenseDelete")
beego.Router("/api/oa/expense/item/add/:id", &controllers.AppOaController{}, "post:ExpenseAddItem")
beego.Router("/api/oa/expense/item/update/:id/:item_id", &controllers.AppOaController{}, "post:ExpenseUpdateItem")
beego.Router("/api/oa/expense/item/delete/:id/:item_id", &controllers.AppOaController{}, "delete:ExpenseDeleteItem")
beego.Router("/api/oa/expense/submit/:id", &controllers.AppOaController{}, "post:ExpenseSubmit")
beego.Router("/api/oa/expense/status/:id", &controllers.AppOaController{}, "post:ExpenseStatus")
beego.Router("/api/oa/expense/records/:id", &controllers.AppOaController{}, "get:ExpenseRecords")
// 通讯录(移动端只读:列表 / 组织树 / 详情)
beego.Router("/api/oa/contact/list", &controllers.AppOaController{}, "get:ContactList")
beego.Router("/api/oa/contact/org-tree", &controllers.AppOaController{}, "get:ContactOrgTree")
+1 -1
View File
@@ -1,4 +1,4 @@
package services
package services
import (
"crypto/rand"