更新oa的数据统计
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user