更新oa的数据统计

This commit is contained in:
2026-08-26 16:15:23 +08:00
parent e60cd00395
commit 20bc65b1f3
11 changed files with 1328 additions and 282 deletions
+148
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"path"
"sort"
"strconv"
"strings"
"time"
@@ -1149,3 +1150,150 @@ func nullableReimburseString(value string) *string {
func uint64Ptr(value uint64) *uint64 {
return &value
}
// Dashboard GET /backend/reimbursements/dashboard — 报销统计(周/月/年 + 近6个月趋势 + 状态分布)
func (c *BackendReimburseController) Dashboard() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
now := time.Now()
y, m, _ := now.Date()
today := time.Date(y, m, now.Day(), 0, 0, 0, 0, now.Location())
tomorrow := today.AddDate(0, 0, 1)
weekday := int(today.Weekday())
if weekday == 0 {
weekday = 7
}
weekStart := today.AddDate(0, 0, -(weekday - 1))
monthStart := time.Date(y, m, 1, 0, 0, 0, 0, now.Location())
yearStart := time.Date(y, 1, 1, 0, 0, 0, 0, now.Location())
var rows []models.BackendReimbursement
_, err := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("tid", claims.TenantId).
Filter("user_id", claims.UserID).
Filter("is_deleted", 0).
All(&rows)
if err != nil {
c.reply(500, "查询失败", nil)
return
}
// ponytail: 个人报销单量小,一次全量查询内存聚合;量大时改 SQL GROUP BY
newStat := func() map[string]interface{} {
return map[string]interface{}{"total": 0.0, "paid": 0.0, "unpaid": 0.0, "count": 0}
}
addTo := func(stat map[string]interface{}, row *models.BackendReimbursement) {
stat["total"] = stat["total"].(float64) + row.TotalAmount
stat["count"] = stat["count"].(int) + 1
if row.Status == 5 { // 已打款视为已报销
stat["paid"] = stat["paid"].(float64) + row.TotalAmount
} else {
stat["unpaid"] = stat["unpaid"].(float64) + row.TotalAmount
}
}
inRange := func(t, start, end time.Time) bool {
return !t.Before(start) && t.Before(end)
}
weekStat, monthStat, yearStat := newStat(), newStat(), newStat()
monthKeys := make([]string, 0, 6)
monthStats := map[string]map[string]interface{}{}
for i := 5; i >= 0; i-- {
key := time.Date(y, m-time.Month(i), 1, 0, 0, 0, 0, now.Location()).Format("2006-01")
monthKeys = append(monthKeys, key)
monthStats[key] = newStat()
}
statusNames := map[int8]string{0: "草稿", 1: "审批中", 2: "已通过", 3: "已驳回", 4: "已撤回", 5: "已打款"}
statusDist := map[string]interface{}{}
for i := range rows {
row := &rows[i]
if inRange(row.ApplyDate, weekStart, tomorrow) {
addTo(weekStat, row)
}
if inRange(row.ApplyDate, monthStart, tomorrow) {
addTo(monthStat, row)
}
if !row.ApplyDate.Before(yearStart) {
addTo(yearStat, row)
}
if st, ok := monthStats[row.ApplyDate.Format("2006-01")]; ok {
addTo(st, row)
}
sk := strconv.Itoa(int(row.Status))
dist, ok := statusDist[sk].(map[string]interface{})
if !ok {
dist = map[string]interface{}{"name": statusNames[row.Status], "count": 0, "amount": 0.0}
statusDist[sk] = dist
}
dist["count"] = dist["count"].(int) + 1
dist["amount"] = dist["amount"].(float64) + row.TotalAmount
}
trend := make([]map[string]interface{}, 0, len(monthKeys))
for _, key := range monthKeys {
st := monthStats[key]
trend = append(trend, map[string]interface{}{
"month": key, "total": st["total"], "paid": st["paid"],
"unpaid": st["unpaid"], "count": st["count"],
})
}
// 报销类型分析:仅统计已打款(status=5)的报销单费用明细
paidIDs := map[uint64]bool{}
for i := range rows {
if rows[i].Status == 5 {
paidIDs[rows[i].ID] = true
}
}
var items []models.BackendReimbursementItem
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("tid", claims.TenantId).
Filter("uid", claims.UserID).
Filter("is_deleted", 0).
All(&items)
if err != nil {
c.reply(500, "查询失败", nil)
return
}
typeAgg := map[string]*struct {
count int
amount float64
}{}
for i := range items {
if !paidIDs[items[i].ReimbursementID] {
continue
}
name := strings.TrimSpace(items[i].ExpenseType)
if name == "" {
name = "未分类"
}
agg, ok := typeAgg[name]
if !ok {
agg = &struct {
count int
amount float64
}{}
typeAgg[name] = agg
}
agg.count++
agg.amount += items[i].Amount
}
expenseTypes := make([]map[string]interface{}, 0, len(typeAgg))
for name, agg := range typeAgg {
expenseTypes = append(expenseTypes, map[string]interface{}{
"name": name, "count": agg.count, "amount": agg.amount,
})
}
sort.Slice(expenseTypes, func(i, j int) bool {
return expenseTypes[i]["amount"].(float64) > expenseTypes[j]["amount"].(float64)
})
c.reply(200, "success", map[string]interface{}{
"week": weekStat, "month": monthStat, "year": yearStat,
"trend": trend, "status_dist": statusDist, "expense_types": expenseTypes,
})
}
+3 -1
View File
@@ -129,6 +129,7 @@ func (c *PlatformAuthController) LoginPlatform() {
// 控制器只做 HTTP 解析与响应编排,业务逻辑放 services 层
token, loginUser, err := services.PlatformAdminLogin(req.Account, req.Password)
if err != nil {
RecordLoginLog(nil, 0, req.Account, "", "", "password", 0, err.Error(), c.Ctx.Input.IP(), c.Ctx.Request.UserAgent())
c.Data["json"] = map[string]interface{}{
"code": 401,
"msg": err.Error(),
@@ -137,6 +138,8 @@ func (c *PlatformAuthController) LoginPlatform() {
return
}
RecordLoginLog(nil, loginUser.ID, loginUser.Account, loginUser.Name, "", "password", 1, "登录成功", c.Ctx.Input.IP(), c.Ctx.Request.UserAgent())
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "登录成功",
@@ -422,4 +425,3 @@ func (c *PlatformAuthController) SendResetCode() {
}
_ = c.ServeJSON()
}
+259
View File
@@ -0,0 +1,259 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformLoginLogController 登录日志(yz_system_login_log,平台端)
type PlatformLoginLogController struct {
beego.Controller
}
func (c *PlatformLoginLogController) 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 *PlatformLoginLogController) 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
}
// List GET /platform/loginLogs?page=1&pageSize=20&keyword=&status=&loginType=&startTime=&endTime=
func (c *PlatformLoginLogController) List() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 200 {
pageSize = 200
}
keyword := strings.TrimSpace(c.GetString("keyword"))
statusStr := strings.TrimSpace(c.GetString("status"))
loginType := strings.TrimSpace(c.GetString("loginType"))
startTimeStr := strings.TrimSpace(c.GetString("startTime"))
endTimeStr := strings.TrimSpace(c.GetString("endTime"))
qs := models.Orm.QueryTable(new(models.SystemLoginLog)).Filter("delete_time__isnull", true)
cond := orm.NewCondition()
needCond := false
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
cond = cond.And("status", st)
needCond = true
}
}
if loginType != "" {
cond = cond.And("login_type", loginType)
needCond = true
}
if keyword != "" {
kw := orm.NewCondition().
Or("account__icontains", keyword).
Or("user_name__icontains", keyword).
Or("tenant_name__icontains", keyword).
Or("ip__icontains", keyword)
if uid, err := strconv.ParseUint(keyword, 10, 64); err == nil && uid > 0 {
kw = kw.Or("user_id", uid)
}
cond = cond.AndCond(kw)
needCond = true
}
if t, err := parseTimeFlexible(startTimeStr); err == nil && !t.IsZero() {
cond = cond.And("create_time__gte", t)
needCond = true
}
if t, err := parseTimeFlexible(endTimeStr); err == nil && !t.IsZero() {
cond = cond.And("create_time__lte", t)
needCond = true
}
if needCond {
qs = qs.SetCond(cond)
}
total, err := qs.Count()
if err != nil {
c.jsonErr(500, 500, "获取登录日志失败: "+err.Error())
return
}
var rows []models.SystemLoginLog
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil {
c.jsonErr(500, 500, "获取登录日志失败: "+err.Error())
return
}
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
item := map[string]interface{}{
"id": rows[i].ID,
"tid": rows[i].Tid,
"user_id": rows[i].UserID,
"account": rows[i].Account,
"user_name": rows[i].UserName,
"tenant_name": rows[i].TenantName,
"login_type": rows[i].LoginType,
"status": rows[i].Status,
"message": rows[i].Message,
"ip": rows[i].IP,
"user_agent": rows[i].UserAgent,
"create_time": rows[i].CreateTime.Format("2006-01-02 15:04:05"),
}
list = append(list, item)
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": total,
},
}
_ = c.ServeJSON()
}
// Detail GET /platform/loginLogs/:id
func (c *PlatformLoginLogController) Detail() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
c.jsonErr(400, 400, "无效ID")
return
}
var row models.SystemLoginLog
err = models.Orm.QueryTable(new(models.SystemLoginLog)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row)
if err != nil {
c.jsonErr(404, 404, "记录不存在")
return
}
out := map[string]interface{}{
"id": row.ID,
"tid": row.Tid,
"user_id": row.UserID,
"account": row.Account,
"user_name": row.UserName,
"tenant_name": row.TenantName,
"login_type": row.LoginType,
"status": row.Status,
"message": row.Message,
"ip": row.IP,
"user_agent": row.UserAgent,
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
_ = c.ServeJSON()
}
// Delete DELETE /platform/loginLogs/:id
func (c *PlatformLoginLogController) Delete() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
c.jsonErr(400, 400, "无效ID")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemLoginLog)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now})
if err != nil {
c.jsonErr(500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
c.jsonErr(404, 404, "记录不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
type platformLoginLogBatchDeletePayload struct {
IDs []uint64 `json:"ids"`
}
// BatchDelete POST /platform/loginLogs/batchDelete
func (c *PlatformLoginLogController) BatchDelete() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
var p platformLoginLogBatchDeletePayload
if err := json.Unmarshal(raw, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
if len(p.IDs) == 0 {
c.jsonErr(400, 400, "请选择要删除的日志")
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemLoginLog)).
Filter("id__in", p.IDs).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now})
if err != nil {
c.jsonErr(500, 500, "批量删除失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "批量删除成功"}
_ = c.ServeJSON()
}
+1
View File
@@ -204,6 +204,7 @@ func RegisterAuthRoutes() {
// 报销与审批管理
beego.Router("/backend/reimbursements", &controllers.BackendReimburseController{}, "get:List;post:Create")
beego.Router("/backend/reimbursements/dashboard", &controllers.BackendReimburseController{}, "get:Dashboard")
beego.Router("/backend/reimbursements/types/manage", &controllers.BackendReimburseController{}, "get:TypesManage")
beego.Router("/backend/reimbursements/types/:type_id", &controllers.BackendReimburseController{}, "post:UpdateType;delete:DeleteType")
beego.Router("/backend/reimbursements/types", &controllers.BackendReimburseController{}, "get:Types;post:CreateType")
+5
View File
@@ -79,6 +79,11 @@ func Register() {
beego.Router("/platform/operationLogs/:id", &controllers.PlatformOperationLogController{}, "get:Detail;delete:Delete")
beego.Router("/platform/operationLogs/batchDelete", &controllers.PlatformOperationLogController{}, "post:BatchDelete")
// 登录日志(yz_system_login_log)
beego.Router("/platform/loginLogs", &controllers.PlatformLoginLogController{}, "get:List")
beego.Router("/platform/loginLogs/batchDelete", &controllers.PlatformLoginLogController{}, "post:BatchDelete")
beego.Router("/platform/loginLogs/:id", &controllers.PlatformLoginLogController{}, "get:Detail;delete:Delete")
// 域名管理(主域名池 / 租户域名)
beego.Router("/platform/domain/pool/index", &controllers.PlatformDomainPoolController{}, "get:Index")
beego.Router("/platform/domain/pool/getEnabledDomains", &controllers.PlatformDomainPoolController{}, "get:GetEnabledDomains")