优化项目管理

This commit is contained in:
2026-09-13 22:31:42 +08:00
parent 2f6cdbb3cd
commit a8c09c1db5
30 changed files with 1654 additions and 346 deletions
+68 -5
View File
@@ -60,10 +60,12 @@ type paybackPayload struct {
ReceiptName string `json:"receipt_name"` // 流水回执文件名(选填)
}
// paybackResp 列表 / 详情响应:items 解析为 JSON 数组透出。
// paybackResp 列表 / 详情响应:items 解析为 JSON 数组透出;
// children 为回款明细(树形子集:该计划下一笔笔实际回款记录)。
type paybackResp struct {
models.TenantCrmPayback
Items json.RawMessage `json:"items"`
Items json.RawMessage `json:"items"`
Children []models.TenantCrmPaybackRecord `json:"children"`
}
// paybackProgress 回款进度行(跨计划摊平的计划明细)。
@@ -109,6 +111,8 @@ func (c *BackendCrmPaybackController) List() {
tenantID := pipelineTenantID(claims)
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
// 数据范围:平台/租户管理员可见全部,其他用户仅本人(负责人或创建人)
cond = crmApplyOwnerScope(cond, claims)
if keyword != "" {
cond = cond.AndCond(orm.NewCondition().
Or("contract_name__contains", keyword).
@@ -138,9 +142,32 @@ func (c *BackendCrmPaybackController) List() {
// 排序:进行中(1) 在前、已完成(2) 在后,同状态内按最新创建优先
_, _ = qs.OrderBy("status", "-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
}
// 组装树形数据:每个回款计划挂上它的回款记录作为明细子集
planIDs := make([]uint64, 0, len(list))
for i := range list {
planIDs = append(planIDs, list[i].ID)
}
recordsByPlan := make(map[uint64][]models.TenantCrmPaybackRecord, len(list))
if len(planIDs) > 0 {
var recs []models.TenantCrmPaybackRecord
_, _ = models.Orm.QueryTable(new(models.TenantCrmPaybackRecord)).
Filter("tenant_id", tenantID).
Filter("payback_id__in", planIDs).
Filter("delete_time__isnull", true).
OrderBy("-received_date", "-id").All(&recs)
for _, r := range recs {
recordsByPlan[r.PaybackID] = append(recordsByPlan[r.PaybackID], r)
}
}
items := make([]paybackResp, 0, len(list))
for i := range list {
items = append(items, buildPaybackResp(&list[i]))
resp := buildPaybackResp(&list[i])
if recs := recordsByPlan[list[i].ID]; len(recs) > 0 {
resp.Children = recs
} else {
resp.Children = []models.TenantCrmPaybackRecord{}
}
items = append(items, resp)
}
// 汇总(当前筛选条件下全部计划的计划总额 / 已回款 / 待回款),用于列表页统计卡片
@@ -271,6 +298,11 @@ func (c *BackendCrmPaybackController) Update() {
pipelineErr(&c.Controller, 404, 404, "回款计划未找到")
return
}
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
pipelineErr(&c.Controller, 403, 403, "无权操作该回款计划数据")
return
}
if p.ContractID > 0 {
row.ContractID = &p.ContractID
@@ -302,20 +334,23 @@ func (c *BackendCrmPaybackController) Update() {
row.Status = pickInt8(p.Status, 1, 2)
}
if p.Items != nil {
itemsJSON, _, err := normalizePaybackItems(p.Items)
itemsJSON, total, err := normalizePaybackItems(p.Items)
if err != nil {
pipelineErr(&c.Controller, 400, 400, "计划明细格式错误")
return
}
row.Items = itemsJSON
row.TotalAmount = total // 计划总额随明细重算
}
applyPaybackReceived(&row)
row.UpdateTime = time.Now()
if _, err := models.Orm.Update(&row); err != nil {
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
// 已回款金额一律以「回款记录」为准重新汇总:
// 明细里不承载实收数据,若此处按明细重算,编辑计划(如调整回款日期)会把已回款冲成 0。
refreshPlanReceived(tenantID, row.ID)
crmWriteLog(tenantID, 4, row.ID, "update", "更新回款计划:"+row.ContractName, claims)
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
}
@@ -340,10 +375,20 @@ func (c *BackendCrmPaybackController) Delete() {
pipelineErr(&c.Controller, 404, 404, "回款计划未找到")
return
}
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
pipelineErr(&c.Controller, 403, 403, "无权操作该回款计划数据")
return
}
if !canDeleteCrmRecord(claims, row.CreateUserID) {
pipelineErr(&c.Controller, 403, 403, "只有创建人、租户管理员或平台管理员可以删除该回款计划")
return
}
// 已完成(回款已收齐)的回款计划不允许删除
if row.Status == 2 {
pipelineErr(&c.Controller, 400, 400, "该回款计划已完成(回款已收齐),不允许删除")
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.TenantCrmPayback)).
Filter("id", id).Filter("tenant_id", tenantID).
@@ -369,6 +414,8 @@ func (c *BackendCrmPaybackController) ProgressList() {
contractID := strings.TrimSpace(c.GetString("contract_id"))
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
// 数据范围:平台/租户管理员可见全部,其他用户仅本人(负责人或创建人)
cond = crmApplyOwnerScope(cond, claims)
if keyword != "" {
cond = cond.AndCond(orm.NewCondition().
Or("contract_name__contains", keyword).
@@ -456,6 +503,11 @@ func (c *BackendCrmPaybackController) Receive() {
pipelineErr(&c.Controller, 404, 404, "回款计划未找到")
return
}
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
pipelineErr(&c.Controller, 403, 403, "无权操作该回款计划数据")
return
}
items := parsePaybackItems(row.Items)
matched := false
for i := range items {
@@ -664,6 +716,8 @@ func (c *BackendCrmPaybackController) RecordList() {
paybackID, _ := c.GetUint64("payback_id")
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
// 数据范围:平台/租户管理员可见全部,其他用户仅本人(负责人或创建人)
cond = crmApplyOwnerScope(cond, claims)
if contractID > 0 {
cond = cond.And("contract_id", contractID)
}
@@ -678,6 +732,7 @@ func (c *BackendCrmPaybackController) RecordList() {
// (已回款由回款记录汇总回写,兼容历史计划)
var totalPlanned, totalReceived float64
planCond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
planCond = crmApplyOwnerScope(planCond, claims)
if contractID > 0 {
planCond = planCond.And("contract_id", contractID)
}
@@ -792,6 +847,14 @@ func (c *BackendCrmPaybackController) DeleteRecord() {
pipelineErr(&c.Controller, 404, 404, "回款记录不存在")
return
}
// 所属回款计划已完成(回款已收齐)时不允许删除回款记录,避免破坏已完成状态
var plan models.TenantCrmPayback
if err := models.Orm.QueryTable(new(models.TenantCrmPayback)).
Filter("id", row.PaybackID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&plan); err == nil && plan.Status == 2 {
pipelineErr(&c.Controller, 400, 400, "所属回款计划已完成(回款已收齐),不允许删除回款记录")
return
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.TenantCrmPaybackRecord)).
Filter("id", id).Filter("tenant_id", tenantID).