优化backend报销管理模块
This commit is contained in:
@@ -4,12 +4,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
@@ -28,14 +30,13 @@ type reimbursementItemPayload struct {
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
Description string `json:"description"`
|
||||
ReceiptFlag int8 `json:"receipt_flag"`
|
||||
ReceiptURL string `json:"receipt_url"`
|
||||
ReceiptFlag int8 `json:"receipt_flag"`
|
||||
ReceiptFileIDs string `json:"receipt_file_ids"`
|
||||
ReceiptURLs string `json:"receipt_urls"`
|
||||
}
|
||||
|
||||
type reimbursementPayload struct {
|
||||
Title string `json:"title"`
|
||||
DepartmentID *uint64 `json:"department_id"`
|
||||
ExpenseType string `json:"expense_type"`
|
||||
ApplyDate string `json:"apply_date"`
|
||||
Description string `json:"description"`
|
||||
Items []reimbursementItemPayload `json:"items"`
|
||||
@@ -96,26 +97,140 @@ func (c *BackendReimburseController) findOwned(id uint64, claims *jwtutil.Claims
|
||||
return row, err
|
||||
}
|
||||
|
||||
func reimbursementItemDTO(item models.BackendReimbursementItem) map[string]interface{} {
|
||||
func parseReceiptFileIDs(raw string) []uint64 {
|
||||
ids := make([]uint64, 0)
|
||||
seen := map[uint64]bool{}
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
|
||||
if err != nil || id == 0 || seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func joinReceiptFileIDs(ids []uint64) string {
|
||||
if len(ids) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
parts = append(parts, strconv.FormatUint(id, 10))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func resolveReceiptFiles(scheme, host string, item models.BackendReimbursementItem) []map[string]interface{} {
|
||||
ids := parseReceiptFileIDs(item.ReceiptFileIDs)
|
||||
if len(ids) == 0 {
|
||||
return []map[string]interface{}{}
|
||||
}
|
||||
urlParts := strings.Split(item.ReceiptURLs, ",")
|
||||
files := make([]map[string]interface{}, 0, len(ids))
|
||||
for i, id := range ids {
|
||||
url := ""
|
||||
if i < len(urlParts) {
|
||||
url = services.ResolvePublicURL(scheme, host, strings.TrimSpace(urlParts[i]))
|
||||
}
|
||||
if url == "" {
|
||||
idCopy := id
|
||||
url = services.ResolveSoftwareDownloadURL(scheme, host, nil, &idCopy)
|
||||
}
|
||||
name := ""
|
||||
var f models.SystemFile
|
||||
if err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).Filter("delete_time__isnull", true).One(&f); err == nil {
|
||||
name = f.Name
|
||||
}
|
||||
if name == "" && url != "" {
|
||||
name = path.Base(strings.Split(url, "?")[0])
|
||||
}
|
||||
files = append(files, map[string]interface{}{
|
||||
"file_id": id, "url": url, "name": name,
|
||||
})
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func syncReceiptFields(item *models.BackendReimbursementItem, scheme, host string) {
|
||||
files := resolveReceiptFiles(scheme, host, *item)
|
||||
if len(files) == 0 {
|
||||
item.ReceiptFileIDs = ""
|
||||
item.ReceiptURLs = ""
|
||||
item.ReceiptFlag = 0
|
||||
return
|
||||
}
|
||||
ids := make([]uint64, 0, len(files))
|
||||
urls := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
if id, ok := file["file_id"].(uint64); ok {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if url, ok := file["url"].(string); ok && url != "" {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
item.ReceiptFileIDs = joinReceiptFileIDs(ids)
|
||||
item.ReceiptURLs = strings.Join(urls, ",")
|
||||
item.ReceiptFlag = 1
|
||||
}
|
||||
|
||||
func applyReceiptToItem(item *models.BackendReimbursementItem, payload reimbursementItemPayload, scheme, host string) {
|
||||
if payload.ReceiptFlag == 0 && strings.TrimSpace(payload.ReceiptFileIDs) == "" {
|
||||
item.ReceiptFileIDs = ""
|
||||
item.ReceiptURLs = ""
|
||||
item.ReceiptFlag = 0
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(payload.ReceiptFileIDs) != "" {
|
||||
item.ReceiptFileIDs = strings.TrimSpace(payload.ReceiptFileIDs)
|
||||
syncReceiptFields(item, scheme, host)
|
||||
return
|
||||
}
|
||||
item.ReceiptFlag = payload.ReceiptFlag
|
||||
item.ReceiptURLs = strings.TrimSpace(payload.ReceiptURLs)
|
||||
if item.ReceiptURLs == "" && item.ReceiptFlag == 0 {
|
||||
item.ReceiptFileIDs = ""
|
||||
}
|
||||
}
|
||||
|
||||
func reimbursementItemDTO(item models.BackendReimbursementItem, scheme, host string) map[string]interface{} {
|
||||
receiptFiles := resolveReceiptFiles(scheme, host, item)
|
||||
receiptURLs := strings.TrimSpace(item.ReceiptURLs)
|
||||
receiptFlag := item.ReceiptFlag
|
||||
if len(receiptFiles) > 0 {
|
||||
receiptFlag = 1
|
||||
if receiptURLs == "" {
|
||||
if url, ok := receiptFiles[0]["url"].(string); ok {
|
||||
receiptURLs = url
|
||||
}
|
||||
}
|
||||
} else if receiptURLs != "" {
|
||||
receiptFlag = 1
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": item.ID, "reimbursement_id": item.ReimbursementID,
|
||||
"expense_type": item.ExpenseType, "expense_date": item.ExpenseDate.Format("2006-01-02"),
|
||||
"amount": item.Amount, "quantity": item.Quantity, "unit_price": item.UnitPrice,
|
||||
"description": item.Description, "receipt_flag": item.ReceiptFlag, "receipt_url": item.ReceiptURL,
|
||||
"description": item.Description, "receipt_flag": receiptFlag,
|
||||
"receipt_file_ids": item.ReceiptFileIDs,
|
||||
"receipt_urls": receiptURLs, "receipt_files": receiptFiles,
|
||||
}
|
||||
}
|
||||
|
||||
func reimbursementDTO(row models.BackendReimbursement, items []models.BackendReimbursementItem) map[string]interface{} {
|
||||
func reimbursementDTO(row models.BackendReimbursement, items []models.BackendReimbursementItem, scheme, host string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"id": row.ID, "tid": row.Tid, "uid": row.UID, "user_id": row.UserID,
|
||||
"department_id": row.DepartmentID, "title": row.Title, "total_amount": row.TotalAmount,
|
||||
"status": row.Status, "expense_type": row.ExpenseType, "apply_date": row.ApplyDate.Format("2006-01-02"),
|
||||
"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,
|
||||
"current_step": row.CurrentStep, "create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
list = append(list, reimbursementItemDTO(item))
|
||||
list = append(list, reimbursementItemDTO(item, scheme, host))
|
||||
}
|
||||
result["items"] = list
|
||||
return result
|
||||
@@ -170,10 +285,8 @@ func (c *BackendReimburseController) List() {
|
||||
"uid": row.UID,
|
||||
"user_id": row.UserID,
|
||||
"department_id": row.DepartmentID,
|
||||
"title": row.Title,
|
||||
"total_amount": row.TotalAmount,
|
||||
"status": row.Status,
|
||||
"expense_type": row.ExpenseType,
|
||||
"apply_date": row.ApplyDate.Format("2006-01-02"),
|
||||
"description": row.Description,
|
||||
"current_approver_id": row.CurrentApproverID,
|
||||
@@ -207,10 +320,11 @@ func (c *BackendReimburseController) Detail() {
|
||||
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, "查询明细失败", nil)
|
||||
c.reply(500, "查询明细失败: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", reimbursementDTO(*row, items))
|
||||
scheme, host := services.PublicRequestBaseURL(&c.Controller)
|
||||
c.reply(200, "success", reimbursementDTO(*row, items, scheme, host))
|
||||
}
|
||||
|
||||
// Create POST /backend/reimbursements
|
||||
@@ -233,8 +347,7 @@ func (c *BackendReimburseController) Create() {
|
||||
uid := uint64(claims.UserID)
|
||||
row := &models.BackendReimbursement{
|
||||
Tid: uint64(claims.TenantId), UID: &uid, UserID: uid, DepartmentID: payload.DepartmentID,
|
||||
Title: "", TotalAmount: 0, Status: 0,
|
||||
ExpenseType: strings.TrimSpace(payload.ExpenseType), ApplyDate: applyDate,
|
||||
TotalAmount: 0, Status: 0, ApplyDate: applyDate,
|
||||
Description: nullableReimburseString(payload.Description),
|
||||
}
|
||||
if err := saveReimbursement(row); err != nil {
|
||||
@@ -419,13 +532,14 @@ func (c *BackendReimburseController) AddItem() {
|
||||
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,
|
||||
Amount: payload.Amount, Quantity: quantity, UnitPrice: payload.UnitPrice,
|
||||
Description: strings.TrimSpace(payload.Description), ReceiptFlag: payload.ReceiptFlag,
|
||||
ReceiptURL: strings.TrimSpace(payload.ReceiptURL),
|
||||
Description: strings.TrimSpace(payload.Description),
|
||||
}
|
||||
applyReceiptToItem(item, payload, scheme, host)
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
c.reply(500, "新增费用明细失败", nil)
|
||||
return
|
||||
@@ -434,7 +548,7 @@ func (c *BackendReimburseController) AddItem() {
|
||||
c.reply(500, "更新报销总额失败", nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", reimbursementItemDTO(*item))
|
||||
c.reply(200, "success", reimbursementItemDTO(*item, scheme, host))
|
||||
}
|
||||
|
||||
// UpdateItem POST /backend/reimbursements/:id/items/:item_id
|
||||
@@ -476,14 +590,14 @@ func (c *BackendReimburseController) UpdateItem() {
|
||||
if quantity <= 0 {
|
||||
quantity = 1
|
||||
}
|
||||
scheme, host := services.PublicRequestBaseURL(&c.Controller)
|
||||
item.ExpenseType = strings.TrimSpace(payload.ExpenseType)
|
||||
item.ExpenseDate = date
|
||||
item.Amount = payload.Amount
|
||||
item.Quantity = quantity
|
||||
item.UnitPrice = payload.UnitPrice
|
||||
item.Description = strings.TrimSpace(payload.Description)
|
||||
item.ReceiptFlag = payload.ReceiptFlag
|
||||
item.ReceiptURL = strings.TrimSpace(payload.ReceiptURL)
|
||||
applyReceiptToItem(item, payload, scheme, host)
|
||||
if _, err := models.Orm.Update(item); err != nil {
|
||||
c.reply(500, "更新费用明细失败", nil)
|
||||
return
|
||||
@@ -492,7 +606,7 @@ func (c *BackendReimburseController) UpdateItem() {
|
||||
c.reply(500, "更新报销总额失败", nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", reimbursementItemDTO(*item))
|
||||
c.reply(200, "success", reimbursementItemDTO(*item, scheme, host))
|
||||
}
|
||||
|
||||
// DeleteItem DELETE /backend/reimbursements/:id/items/:item_id
|
||||
@@ -583,6 +697,65 @@ func (c *BackendReimburseController) Submit() {
|
||||
c.reply(200, "success", nil)
|
||||
}
|
||||
|
||||
type reimbursementStatusPayload struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
var manualConfirmStatuses = map[int8]bool{
|
||||
0: true, // 草稿
|
||||
1: true, // 审批中
|
||||
2: true, // 已通过
|
||||
3: true, // 已驳回
|
||||
4: true, // 已撤回
|
||||
5: true, // 已打款
|
||||
}
|
||||
|
||||
// UpdateStatus POST /backend/reimbursements/:id/status — 手动确认更改单据状态
|
||||
func (c *BackendReimburseController) UpdateStatus() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
c.reply(401, "未登录或无权限", nil)
|
||||
return
|
||||
}
|
||||
id, ok := c.getID()
|
||||
if !ok {
|
||||
c.reply(400, "无效ID", nil)
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.reply(400, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
var payload reimbursementStatusPayload
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
c.reply(400, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
if !manualConfirmStatuses[payload.Status] {
|
||||
c.reply(400, "不支持的状态值", nil)
|
||||
return
|
||||
}
|
||||
row, err := c.findOwned(id, claims)
|
||||
if err != nil {
|
||||
c.reply(404, "报销单不存在", nil)
|
||||
return
|
||||
}
|
||||
if row.Status == payload.Status {
|
||||
c.reply(200, "success", map[string]interface{}{"status": row.Status})
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
|
||||
"status": payload.Status, "update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.reply(500, "更新状态失败", nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", map[string]interface{}{"status": payload.Status})
|
||||
}
|
||||
|
||||
// Records GET /backend/reimbursements/:id/records
|
||||
func (c *BackendReimburseController) Records() {
|
||||
claims, ok := c.claims()
|
||||
@@ -613,25 +786,215 @@ func (c *BackendReimburseController) Records() {
|
||||
c.reply(200, "success", rows)
|
||||
}
|
||||
|
||||
// Types GET /backend/reimbursements/types
|
||||
type expenseTypePayload struct {
|
||||
Name string `json:"name"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsActive *int8 `json:"is_active"`
|
||||
}
|
||||
|
||||
func expenseTypeScopeCond(tenantID int) *orm.Condition {
|
||||
cond := orm.NewCondition()
|
||||
return cond.Or("tid", 0).Or("tid", tenantID)
|
||||
}
|
||||
|
||||
func expenseTypeDTO(row models.BackendReimbursementExpenseType, tenantID int) map[string]interface{} {
|
||||
isGlobal := row.Tid == 0
|
||||
return map[string]interface{}{
|
||||
"id": row.ID, "tid": row.Tid, "name": row.Name,
|
||||
"sort_order": row.SortOrder, "is_active": row.IsActive,
|
||||
"is_global": isGlobal, "editable": !isGlobal && row.Tid == uint64(tenantID),
|
||||
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BackendReimburseController) queryExpenseTypes(tenantID int, activeOnly bool) ([]models.BackendReimbursementExpenseType, error) {
|
||||
qs := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||
SetCond(expenseTypeScopeCond(tenantID)).
|
||||
Filter("is_deleted", 0)
|
||||
if activeOnly {
|
||||
qs = qs.Filter("is_active", 1)
|
||||
}
|
||||
var rows []models.BackendReimbursementExpenseType
|
||||
_, err := qs.OrderBy("sort_order", "id").All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []models.BackendReimbursementExpenseType{}
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (c *BackendReimburseController) getTypeID() (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":type_id"), 10, 64)
|
||||
return id, err == nil && id > 0
|
||||
}
|
||||
|
||||
func (c *BackendReimburseController) findTenantExpenseType(id uint64, tenantID int) (*models.BackendReimbursementExpenseType, error) {
|
||||
row := &models.BackendReimbursementExpenseType{}
|
||||
err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tenantID).
|
||||
Filter("is_deleted", 0).
|
||||
One(row)
|
||||
return row, err
|
||||
}
|
||||
|
||||
func (c *BackendReimburseController) parseExpenseTypePayload() (expenseTypePayload, error) {
|
||||
var payload expenseTypePayload
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
return payload, err
|
||||
}
|
||||
err = json.Unmarshal(raw, &payload)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
// Types GET /backend/reimbursements/types — 下拉选项:全局通用 + 当前租户专有(仅启用)
|
||||
func (c *BackendReimburseController) Types() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
c.reply(401, "未登录或无权限", nil)
|
||||
return
|
||||
}
|
||||
var rows []models.BackendReimbursementExpenseType
|
||||
_, err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||
Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0).
|
||||
OrderBy("sort_order", "id").All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
rows, err := c.queryExpenseTypes(claims.TenantId, true)
|
||||
if err != nil {
|
||||
c.reply(500, "查询费用类型失败", nil)
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []models.BackendReimbursementExpenseType{}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, expenseTypeDTO(row, claims.TenantId))
|
||||
}
|
||||
c.reply(200, "success", rows)
|
||||
c.reply(200, "success", list)
|
||||
}
|
||||
|
||||
// TypesManage GET /backend/reimbursements/types/manage — 管理列表:含禁用项,标注是否可编辑
|
||||
func (c *BackendReimburseController) TypesManage() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
c.reply(401, "未登录或无权限", nil)
|
||||
return
|
||||
}
|
||||
rows, err := c.queryExpenseTypes(claims.TenantId, false)
|
||||
if err != nil {
|
||||
c.reply(500, "查询费用类型失败", nil)
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, expenseTypeDTO(row, claims.TenantId))
|
||||
}
|
||||
c.reply(200, "success", list)
|
||||
}
|
||||
|
||||
// CreateType POST /backend/reimbursements/types — 租户新增专有费项
|
||||
func (c *BackendReimburseController) CreateType() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
c.reply(401, "未登录或无权限", nil)
|
||||
return
|
||||
}
|
||||
payload, err := c.parseExpenseTypePayload()
|
||||
if err != nil {
|
||||
c.reply(400, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(payload.Name)
|
||||
if name == "" {
|
||||
c.reply(400, "费项名称不能为空", nil)
|
||||
return
|
||||
}
|
||||
isActive := int8(1)
|
||||
if payload.IsActive != nil {
|
||||
isActive = *payload.IsActive
|
||||
}
|
||||
uid := uint64(claims.UserID)
|
||||
row := &models.BackendReimbursementExpenseType{
|
||||
Tid: uint64(claims.TenantId), UID: &uid,
|
||||
Name: name, SortOrder: payload.SortOrder, IsActive: isActive,
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||
c.reply(400, "该费项名称已存在", nil)
|
||||
return
|
||||
}
|
||||
c.reply(500, "新增费项失败", nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", expenseTypeDTO(*row, claims.TenantId))
|
||||
}
|
||||
|
||||
// UpdateType POST /backend/reimbursements/types/:type_id — 仅可修改本租户专有费项
|
||||
func (c *BackendReimburseController) UpdateType() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
c.reply(401, "未登录或无权限", nil)
|
||||
return
|
||||
}
|
||||
id, ok := c.getTypeID()
|
||||
if !ok {
|
||||
c.reply(400, "无效ID", nil)
|
||||
return
|
||||
}
|
||||
row, err := c.findTenantExpenseType(id, claims.TenantId)
|
||||
if err != nil {
|
||||
c.reply(404, "费项不存在或无权修改", nil)
|
||||
return
|
||||
}
|
||||
payload, err := c.parseExpenseTypePayload()
|
||||
if err != nil {
|
||||
c.reply(400, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
if name := strings.TrimSpace(payload.Name); name != "" {
|
||||
row.Name = name
|
||||
}
|
||||
if payload.SortOrder >= 0 {
|
||||
row.SortOrder = payload.SortOrder
|
||||
}
|
||||
if payload.IsActive != nil {
|
||||
row.IsActive = *payload.IsActive
|
||||
}
|
||||
now := time.Now()
|
||||
row.UpdateTime = &now
|
||||
if _, err := models.Orm.Update(row); err != nil {
|
||||
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||
c.reply(400, "该费项名称已存在", nil)
|
||||
return
|
||||
}
|
||||
c.reply(500, "更新费项失败", nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", expenseTypeDTO(*row, claims.TenantId))
|
||||
}
|
||||
|
||||
// DeleteType DELETE /backend/reimbursements/types/:type_id — 仅可删除本租户专有费项
|
||||
func (c *BackendReimburseController) DeleteType() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
c.reply(401, "未登录或无权限", nil)
|
||||
return
|
||||
}
|
||||
id, ok := c.getTypeID()
|
||||
if !ok {
|
||||
c.reply(400, "无效ID", nil)
|
||||
return
|
||||
}
|
||||
row, err := c.findTenantExpenseType(id, claims.TenantId)
|
||||
if err != nil {
|
||||
c.reply(404, "费项不存在或无权删除", nil)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||
Filter("id", row.ID).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.reply(500, "删除费项失败", nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", nil)
|
||||
}
|
||||
|
||||
// Flows GET /backend/reimbursements/flows
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// PlatformReimburseExpenseTypeController 平台维护全局通用报销费项(tid=0)。
|
||||
type PlatformReimburseExpenseTypeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformReimburseExpenseTypeController) platformClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "platform" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *PlatformReimburseExpenseTypeController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *PlatformReimburseExpenseTypeController) ok(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func globalExpenseTypeDTO(row models.BackendReimbursementExpenseType) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID, "tid": row.Tid, "name": row.Name,
|
||||
"sort_order": row.SortOrder, "is_active": row.IsActive,
|
||||
"is_global": true, "editable": true,
|
||||
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PlatformReimburseExpenseTypeController) findGlobalType(id uint64) (*models.BackendReimbursementExpenseType, error) {
|
||||
row := &models.BackendReimbursementExpenseType{}
|
||||
err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||
Filter("id", id).Filter("tid", 0).Filter("is_deleted", 0).One(row)
|
||||
return row, err
|
||||
}
|
||||
|
||||
// List GET /platform/reimburseExpenseTypes
|
||||
func (c *PlatformReimburseExpenseTypeController) List() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.BackendReimbursementExpenseType
|
||||
_, err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||
Filter("tid", 0).Filter("is_deleted", 0).
|
||||
OrderBy("sort_order", "id").All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []models.BackendReimbursementExpenseType{}
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, globalExpenseTypeDTO(row))
|
||||
}
|
||||
c.ok(list)
|
||||
}
|
||||
|
||||
type platformExpenseTypePayload struct {
|
||||
Name string `json:"name"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsActive *int8 `json:"is_active"`
|
||||
}
|
||||
|
||||
// Create POST /platform/reimburseExpenseTypes
|
||||
func (c *PlatformReimburseExpenseTypeController) Create() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p platformExpenseTypePayload
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(p.Name)
|
||||
if name == "" {
|
||||
c.jsonErr(400, 400, "费项名称不能为空")
|
||||
return
|
||||
}
|
||||
isActive := int8(1)
|
||||
if p.IsActive != nil {
|
||||
isActive = *p.IsActive
|
||||
}
|
||||
uid := uint64(claims.UserID)
|
||||
row := models.BackendReimbursementExpenseType{
|
||||
Tid: 0, UID: &uid, Name: name, SortOrder: p.SortOrder, IsActive: isActive,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||
c.jsonErr(400, 400, "该费项名称已存在")
|
||||
return
|
||||
}
|
||||
c.jsonErr(500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
row.ID = uint64(id)
|
||||
c.ok(globalExpenseTypeDTO(row))
|
||||
}
|
||||
|
||||
// Update POST /platform/reimburseExpenseTypes/:id
|
||||
func (c *PlatformReimburseExpenseTypeController) Update() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
row, err := c.findGlobalType(id)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "费项不存在")
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p platformExpenseTypePayload
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if name := strings.TrimSpace(p.Name); name != "" {
|
||||
row.Name = name
|
||||
}
|
||||
if p.SortOrder >= 0 {
|
||||
row.SortOrder = p.SortOrder
|
||||
}
|
||||
if p.IsActive != nil {
|
||||
row.IsActive = *p.IsActive
|
||||
}
|
||||
now := time.Now()
|
||||
row.UpdateTime = &now
|
||||
if _, err := models.Orm.Update(row); err != nil {
|
||||
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||
c.jsonErr(400, 400, "该费项名称已存在")
|
||||
return
|
||||
}
|
||||
c.jsonErr(500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(globalExpenseTypeDTO(*row))
|
||||
}
|
||||
|
||||
// Delete DELETE /platform/reimburseExpenseTypes/:id
|
||||
func (c *PlatformReimburseExpenseTypeController) Delete() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
row, err := c.findGlobalType(id)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "费项不存在")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||
Filter("id", row.ID).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(nil)
|
||||
}
|
||||
Reference in New Issue
Block a user