优化回款功能

This commit is contained in:
2026-09-13 14:09:34 +08:00
parent aea53e0766
commit 2f6cdbb3cd
13 changed files with 855 additions and 668 deletions
+187 -1
View File
@@ -135,7 +135,8 @@ func (c *BackendCrmPaybackController) List() {
total, _ := qs.Count()
var list []models.TenantCrmPayback
if total > 0 {
_, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
// 排序:进行中(1) 在前、已完成(2) 在后,同状态内按最新创建优先
_, _ = qs.OrderBy("status", "-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
}
items := make([]paybackResp, 0, len(list))
for i := range list {
@@ -644,3 +645,188 @@ func (c *BackendCrmPaybackController) fillFromContract(tenantID string, contract
func formatAmount(n float64) string {
return strconv.FormatFloat(round2(n), 'f', 2, 64)
}
// ========================== 回款记录(回款进度流水) ==========================
//
// 以「回款计划」为先导条件:每次「新建回款」追加一条记录,构成回款进度列表(一笔笔实际到账);
// 计划表的 received_amount / status 由本表金额汇总回写(refreshPlanReceived)。
// RecordList GET /backend/crm/payback/record/list
// 回款进度:按合同 / 计划返回一笔笔实际回款记录,并给出计划总额与已回款汇总。
func (c *BackendCrmPaybackController) RecordList() {
claims, err := pipelineClaims(&c.Controller)
if err != nil {
pipelineErr(&c.Controller, 401, 401, err.Error())
return
}
tenantID := pipelineTenantID(claims)
contractID, _ := c.GetUint64("contract_id")
paybackID, _ := c.GetUint64("payback_id")
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
if contractID > 0 {
cond = cond.And("contract_id", contractID)
}
if paybackID > 0 {
cond = cond.And("payback_id", paybackID)
}
list := make([]models.TenantCrmPaybackRecord, 0)
_, _ = models.Orm.QueryTable(new(models.TenantCrmPaybackRecord)).
SetCond(cond).OrderBy("-received_date", "-id").All(&list)
// 汇总:回款计划的计划总额与已回款;带 contract_id 时只统计该合同,否则统计全部
// (已回款由回款记录汇总回写,兼容历史计划)
var totalPlanned, totalReceived float64
planCond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
if contractID > 0 {
planCond = planCond.And("contract_id", contractID)
}
var plans []models.TenantCrmPayback
_, _ = models.Orm.QueryTable(new(models.TenantCrmPayback)).SetCond(planCond).All(&plans)
for _, p := range plans {
totalPlanned = round2(totalPlanned + p.TotalAmount)
totalReceived = round2(totalReceived + p.ReceivedAmount)
}
pipelineOk(&c.Controller, map[string]interface{}{
"list": list, "total": len(list),
"total_planned": totalPlanned, "total_received": totalReceived,
})
}
// CreateRecord POST /backend/crm/payback/record
// 新建回款:以回款计划为先导,追加一条实际回款记录,并回写计划的已回款金额与状态。
func (c *BackendCrmPaybackController) CreateRecord() {
claims, err := pipelineClaims(&c.Controller)
if err != nil {
pipelineErr(&c.Controller, 401, 401, err.Error())
return
}
raw, _ := io.ReadAll(c.Ctx.Request.Body)
var p struct {
PaybackID uint64 `json:"payback_id"`
Amount float64 `json:"amount"`
ReceivedDate string `json:"received_date"`
PayMethod int8 `json:"pay_method"`
ReceiptURL string `json:"receipt_url"`
ReceiptName string `json:"receipt_name"`
Remark string `json:"remark"`
}
if err := json.Unmarshal(raw, &p); err != nil {
pipelineErr(&c.Controller, 400, 400, "参数错误")
return
}
if p.PaybackID == 0 {
pipelineErr(&c.Controller, 400, 400, "请先选择回款计划")
return
}
if round2(p.Amount) <= 0 {
pipelineErr(&c.Controller, 400, 400, "请填写本次回款金额")
return
}
tenantID := pipelineTenantID(claims)
// 回款计划必须存在且属于当前租户(回款进度以计划为先导条件)
var plan models.TenantCrmPayback
if err := models.Orm.QueryTable(new(models.TenantCrmPayback)).
Filter("id", p.PaybackID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&plan); err != nil {
pipelineErr(&c.Controller, 404, 404, "回款计划不存在")
return
}
payMethod := p.PayMethod
if payMethod <= 0 {
payMethod = plan.PayMethod
}
var contractID *uint64
if plan.ContractID != nil {
cid := *plan.ContractID
contractID = &cid
}
now := time.Now()
row := models.TenantCrmPaybackRecord{
TenantID: tenantID,
ContractID: contractID,
PaybackID: plan.ID,
Amount: round2(p.Amount),
ReceivedDate: strings.TrimSpace(p.ReceivedDate),
PayMethod: payMethod,
ReceiptURL: strings.TrimSpace(p.ReceiptURL),
ReceiptName: strings.TrimSpace(p.ReceiptName),
Remark: strings.TrimSpace(p.Remark),
OwnerUserID: plan.OwnerUserID,
OwnerUserName: plan.OwnerUserName,
CreateUserID: pipelineUID(claims),
CreateTime: now,
UpdateTime: now,
}
id, ierr := models.Orm.Insert(&row)
if ierr != nil {
pipelineErr(&c.Controller, 500, 500, "保存失败: "+ierr.Error())
return
}
refreshPlanReceived(tenantID, plan.ID)
if contractID != nil {
crmWriteLog(tenantID, 3, *contractID, "payback",
fmt.Sprintf("新建回款:%s 元(%s)", formatAmount(row.Amount), row.ReceivedDate), claims)
}
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
}
// DeleteRecord DELETE /backend/crm/payback/record/:id
// 删除一条回款记录(软删除),并回写计划的已回款金额与状态。
func (c *BackendCrmPaybackController) DeleteRecord() {
claims, err := pipelineClaims(&c.Controller)
if err != nil {
pipelineErr(&c.Controller, 401, 401, err.Error())
return
}
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if id == 0 {
pipelineErr(&c.Controller, 400, 400, "无效的ID")
return
}
tenantID := pipelineTenantID(claims)
var row models.TenantCrmPaybackRecord
if err := models.Orm.QueryTable(new(models.TenantCrmPaybackRecord)).
Filter("id", id).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&row); err != nil {
pipelineErr(&c.Controller, 404, 404, "回款记录不存在")
return
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.TenantCrmPaybackRecord)).
Filter("id", id).Filter("tenant_id", tenantID).
Update(orm.Params{"delete_time": now, "update_time": now}); err != nil {
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
refreshPlanReceived(tenantID, row.PaybackID)
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
}
// refreshPlanReceived 按回款记录重算计划的已回款金额,并同步计划状态(1进行中 / 2已完成)。
func refreshPlanReceived(tenantID string, paybackID uint64) {
if paybackID == 0 {
return
}
var rows []orm.Params
sql := "SELECT IFNULL(SUM(amount),0) AS received FROM " +
new(models.TenantCrmPaybackRecord).TableName() +
" WHERE tenant_id = ? AND payback_id = ? AND delete_time IS NULL"
if _, err := models.Orm.Raw(sql, tenantID, paybackID).Values(&rows); err != nil || len(rows) == 0 {
return
}
received := round2(toFloat64(rows[0]["received"]))
var plan models.TenantCrmPayback
if err := models.Orm.QueryTable(new(models.TenantCrmPayback)).
Filter("id", paybackID).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&plan); err != nil {
return
}
status := int8(1)
if plan.TotalAmount > 0 && received+0.005 >= plan.TotalAmount {
status = 2
}
_, _ = models.Orm.QueryTable(new(models.TenantCrmPayback)).
Filter("id", paybackID).Filter("tenant_id", tenantID).
Update(orm.Params{"received_amount": received, "status": status, "update_time": time.Now()})
}