优化小程序报销模块
This commit is contained in:
1 parent
1e9666cefd
commit
48f3bd997c
21 files changed
+5738
-1750
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user