优化小程序报销模块

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

+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)