修复个人中心问题
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -61,9 +62,11 @@ type projectPayload struct {
|
||||
UpstreamAmount float64 `json:"upstream_amount"`
|
||||
DownstreamAmount float64 `json:"downstream_amount"`
|
||||
// Amount 为旧「项目金额」,未传上游金额时回退使用(兼容未升级的调用方)
|
||||
Amount float64 `json:"amount"`
|
||||
Status int8 `json:"status"`
|
||||
StartDate string `json:"start_date"`
|
||||
Amount float64 `json:"amount"`
|
||||
Status int8 `json:"status"`
|
||||
// Reason 异常原因:状态改为「异常(5)」时必填,会写入操作日志便于追溯
|
||||
Reason string `json:"reason"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
// 上下游对接人:仅姓名 + 电话;Contact* 为旧字段,未传新字段时回退使用
|
||||
UpstreamContact string `json:"upstream_contact"`
|
||||
@@ -222,6 +225,8 @@ func (c *BackendCrmProjectController) List() {
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
status := strings.TrimSpace(c.GetString("status"))
|
||||
ownerID := strings.TrimSpace(c.GetString("owner_user_id"))
|
||||
// 年度筛选:按项目开始时间所在年份过滤(该时间由关联合同运行时汇总,见下方内存过滤)
|
||||
year := strings.TrimSpace(c.GetString("year"))
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
|
||||
@@ -251,65 +256,79 @@ func (c *BackendCrmProjectController) List() {
|
||||
return models.Orm.QueryTable(new(models.TenantCrmProject)).SetCond(cond)
|
||||
}
|
||||
total, _ := projectQuery(buildCond()).Count()
|
||||
// 年度筛选项:按全部候选项目的开始时间汇总(不受当次年度筛选影响,保证下拉里始终能看到所有年份)
|
||||
years := []int{}
|
||||
var list []models.TenantCrmProject
|
||||
if total > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
if status != "" {
|
||||
// 已按状态筛选:结果只含单一状态,直接按新→旧分页
|
||||
if _, err := projectQuery(buildCond()).OrderBy("-id").Offset(offset).Limit(pageSize).All(&list); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
// 排序与年度筛选都依赖「项目开始时间」:取该项目所有关联合同中最早的生效日期
|
||||
// (无生效日期取签订日期),未关联合同时回退旧的手工列 start_date。
|
||||
// 该值是运行时汇总的只读字段(不落库),无法交给 SQL,因此先取全量候选、汇总后在内存里筛选与排序。
|
||||
if _, err := projectQuery(buildCond()).All(&list); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
attachProjectContractPeriod(tenantID, list)
|
||||
startOf := func(p *models.TenantCrmProject) time.Time {
|
||||
if p.ContractStartDate != nil {
|
||||
return *p.ContractStartDate
|
||||
}
|
||||
} else {
|
||||
// 列表排列:按状态分组展示 —— 未开始(1) → 异常(5) → 进行中(2) → 已暂停(4) → 已完成(3),
|
||||
// 其余状态(NULL / 历史异常值)沉底;组内仍按新→旧。
|
||||
// 先统计各组数量,把当前页窗口换算成各组区间,再分别取数按序拼接,保证翻页后整体顺序一致。
|
||||
groupConds := []*orm.Condition{
|
||||
orm.NewCondition().And("status", 1), // 未开始
|
||||
orm.NewCondition().And("status", 5), // 异常
|
||||
orm.NewCondition().And("status", 2), // 进行中
|
||||
orm.NewCondition().And("status", 4), // 已暂停
|
||||
orm.NewCondition().And("status", 3), // 已完成
|
||||
orm.NewCondition().AndNot("status__in", []int8{1, 2, 3, 4, 5}).Or("status__isnull", true),
|
||||
if p.StartDate != nil {
|
||||
return *p.StartDate
|
||||
}
|
||||
end := offset + pageSize
|
||||
if end > int(total) {
|
||||
end = int(total)
|
||||
}
|
||||
cursor := 0
|
||||
for _, gc := range groupConds {
|
||||
if cursor >= end {
|
||||
break
|
||||
}
|
||||
// 注意:beego 的 AndCond 会修改接收者,这里用新条件对象包装,避免循环内条件累积
|
||||
groupCond := orm.NewCondition().AndCond(buildCond()).AndCond(gc)
|
||||
cnt, _ := projectQuery(groupCond).Count()
|
||||
groupTotal := int(cnt)
|
||||
if groupTotal == 0 {
|
||||
continue
|
||||
}
|
||||
groupStart := cursor
|
||||
groupEnd := cursor + groupTotal
|
||||
cursor = groupEnd
|
||||
// 当前页与本组区间的交集
|
||||
from, to := max(offset, groupStart), min(end, groupEnd)
|
||||
if to <= from {
|
||||
continue
|
||||
}
|
||||
var part []models.TenantCrmProject
|
||||
if _, err := projectQuery(groupCond).OrderBy("-id").Offset(from - groupStart).Limit(to - from).All(&part); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
list = append(list, part...)
|
||||
return time.Time{}
|
||||
}
|
||||
yearSet := make(map[int]struct{})
|
||||
for i := range list {
|
||||
if d := startOf(&list[i]); !d.IsZero() {
|
||||
yearSet[d.Year()] = struct{}{}
|
||||
}
|
||||
}
|
||||
years = make([]int, 0, len(yearSet))
|
||||
for y := range yearSet {
|
||||
years = append(years, y)
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(years)))
|
||||
|
||||
// 年度筛选:仅保留开始时间落在该年度的项目(无开始时间的项目不参与年度筛选)
|
||||
if year != "" {
|
||||
if want, err := strconv.Atoi(year); err == nil && want > 0 {
|
||||
kept := make([]models.TenantCrmProject, 0, len(list))
|
||||
for i := range list {
|
||||
if d := startOf(&list[i]); !d.IsZero() && d.Year() == want {
|
||||
kept = append(kept, list[i])
|
||||
}
|
||||
}
|
||||
list = kept
|
||||
total = int64(len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// 排列:按项目开始时间倒序,最新开始的项目排最前;
|
||||
// 无开始时间的项目沉底,时间相同按 id 倒序,保证翻页顺序稳定。
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
ti, tj := startOf(&list[i]), startOf(&list[j])
|
||||
if ti.IsZero() != tj.IsZero() {
|
||||
return !ti.IsZero()
|
||||
}
|
||||
if !ti.Equal(tj) {
|
||||
return ti.After(tj)
|
||||
}
|
||||
return list[i].ID > list[j].ID
|
||||
})
|
||||
offset := (page - 1) * pageSize
|
||||
if offset > len(list) {
|
||||
offset = len(list)
|
||||
}
|
||||
end := offset + pageSize
|
||||
if end > len(list) {
|
||||
end = len(list)
|
||||
}
|
||||
list = list[offset:end]
|
||||
}
|
||||
attachBusinessName(tenantID, list)
|
||||
// 项目起止时间由关联合同汇总(不再依赖手工维护的开始 / 结束日期)
|
||||
attachProjectContractPeriod(tenantID, list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
||||
"list": list, "total": total, "years": years,
|
||||
"page": page, "pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -529,6 +548,8 @@ func (c *BackendCrmProjectController) Update() {
|
||||
proj.DownstreamPhone = downPhone
|
||||
proj.ContactPerson = upContact
|
||||
proj.ContactPhone = upPhone
|
||||
// 状态变更(编辑表单亦可改状态):先记住原状态,供操作日志记录流转
|
||||
prevStatus := proj.Status
|
||||
if p.Status != 0 {
|
||||
proj.Status = p.Status
|
||||
}
|
||||
@@ -539,6 +560,14 @@ func (c *BackendCrmProjectController) Update() {
|
||||
proj.Products = p.Products
|
||||
proj.UpdateTime = time.Now()
|
||||
|
||||
// 状态改为「异常」时必须写明原因(与状态流转同一口径),否则日志无法追溯异常成因
|
||||
statusChanged := p.Status != 0 && p.Status != prevStatus
|
||||
reason := strings.TrimSpace(p.Reason)
|
||||
if p.Status == 5 && prevStatus != 5 && reason == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "请填写项目异常原因")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := models.Orm.Update(&proj); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
@@ -554,6 +583,14 @@ func (c *BackendCrmProjectController) Update() {
|
||||
if proj.DocCategoryID > 0 {
|
||||
_ = services.RenameCrmProjectDocCategory(claims.TenantId, proj.DocCategoryID, proj.ProjectName)
|
||||
}
|
||||
// 编辑表单里改了状态同样要留痕(含异常原因)
|
||||
if statusChanged {
|
||||
logText := "项目状态:" + projectStatusName(prevStatus) + " → " + projectStatusName(proj.Status)
|
||||
if proj.Status == 5 && reason != "" {
|
||||
logText += ";异常原因:" + reason
|
||||
}
|
||||
crmWriteLog(tenantID, 3, proj.ID, "status", logText, claims)
|
||||
}
|
||||
crmWriteLog(tenantID, 3, proj.ID, "update", "更新项目:"+proj.ProjectName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": proj.ID})
|
||||
}
|
||||
@@ -730,7 +767,8 @@ func (c *BackendCrmProjectController) ChangeStatus() {
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
Status int8 `json:"status"`
|
||||
Status int8 `json:"status"`
|
||||
Reason string `json:"reason"` // 异常原因:status=5 时必填
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
@@ -740,6 +778,12 @@ func (c *BackendCrmProjectController) ChangeStatus() {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的状态值")
|
||||
return
|
||||
}
|
||||
// 置为「异常」时必须写明原因,日志里要能看出是什么导致的
|
||||
reason := strings.TrimSpace(p.Reason)
|
||||
if p.Status == 5 && reason == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "请填写项目异常原因")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var proj models.TenantCrmProject
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
@@ -763,8 +807,11 @@ func (c *BackendCrmProjectController) ChangeStatus() {
|
||||
pipelineErr(&c.Controller, 500, 500, "状态更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 3, proj.ID, "status",
|
||||
"项目状态流转:"+projectStatusName(proj.Status)+" → "+projectStatusName(p.Status), claims)
|
||||
logText := "项目状态流转:" + projectStatusName(proj.Status) + " → " + projectStatusName(p.Status)
|
||||
if p.Status == 5 {
|
||||
logText += ";异常原因:" + reason
|
||||
}
|
||||
crmWriteLog(tenantID, 3, proj.ID, "status", logText, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": proj.ID, "status": p.Status})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user