批量更新功能
This commit is contained in:
@@ -21,15 +21,37 @@ type BackendCrmProjectController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// crmProjectStatusDone 项目状态「已完成」(1未开始 / 2进行中 / 3已完成 / 4已暂停 / 5异常):
|
||||
// 列表排列时「已完成」沉底展示,与前端筛选传参一致使用字符串值。
|
||||
const crmProjectStatusDone = "3"
|
||||
|
||||
// projectStatusName 项目状态文案(用于操作日志)。
|
||||
func projectStatusName(s int8) string {
|
||||
switch s {
|
||||
case 1:
|
||||
return "未开始"
|
||||
case 2:
|
||||
return "进行中"
|
||||
case 3:
|
||||
return "已完成"
|
||||
case 4:
|
||||
return "已暂停"
|
||||
case 5:
|
||||
return "异常"
|
||||
}
|
||||
return "未知"
|
||||
}
|
||||
|
||||
type projectPayload struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
ProjectNo string `json:"project_no"`
|
||||
// 项目类型:1=单一项目(仅上游客户) 2=上下游项目(上游客户 + 下游供应商)
|
||||
// 3=上中游项目(上游 A 公司 + 中游 B 公司,下游固定为本公司)
|
||||
ProjectType int8 `json:"project_type"`
|
||||
CustomerID uint64 `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
SupplierID uint64 `json:"supplier_id"`
|
||||
SupplierRefType int8 `json:"supplier_ref_type"` // 下游主体来源:1客户 2供应商
|
||||
SupplierRefType int8 `json:"supplier_ref_type"` // 对方主体来源:1客户 2供应商(下游 / 中游)
|
||||
SupplierName string `json:"supplier_name"`
|
||||
SupplierIndustry string `json:"supplier_industry"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
@@ -57,12 +79,13 @@ type projectPayload struct {
|
||||
}
|
||||
|
||||
// normalizeProjectFields 归一项目类型、上下游金额与上下游对接人:
|
||||
// - 项目类型仅识别 2=上下游,其余一律按 1=单一项目处理;
|
||||
// - 项目类型仅识别 2=上下游、3=上中游,其余一律按 1=单一项目处理;
|
||||
// - 上游金额 / 上游对接人缺省时回退旧的 amount / contact_* 字段(兼容未升级的调用方);
|
||||
// - 单一项目不存在下游,下游金额与下游对接人一并清空。
|
||||
// - 单一项目不存在上下游,下游金额与下游对接人一并清空;
|
||||
// - 3=上中游项目(上游 A 公司 + 中游 B 公司,下游固定为本公司)的中游主体复用下游字段存储。
|
||||
func normalizeProjectFields(p *projectPayload) (pt int8, upstream, downstream float64, upContact, upPhone, downContact, downPhone string) {
|
||||
pt = p.ProjectType
|
||||
if pt != 2 {
|
||||
if pt != 2 && pt != 3 {
|
||||
pt = 1
|
||||
}
|
||||
upstream = p.UpstreamAmount
|
||||
@@ -71,7 +94,8 @@ func normalizeProjectFields(p *projectPayload) (pt int8, upstream, downstream fl
|
||||
}
|
||||
upContact = strings.TrimSpace(firstNonEmpty(p.UpstreamContact, p.ContactPerson))
|
||||
upPhone = strings.TrimSpace(firstNonEmpty(p.UpstreamPhone, p.ContactPhone))
|
||||
if pt == 2 {
|
||||
// 上下游项目:对方为下游(供应商/客户);上中游项目:对方为中游(中间方)——两者均落在 downstream_* 字段
|
||||
if pt == 2 || pt == 3 {
|
||||
downstream = p.DownstreamAmount
|
||||
downContact = strings.TrimSpace(p.DownstreamContact)
|
||||
downPhone = strings.TrimSpace(p.DownstreamPhone)
|
||||
@@ -179,29 +203,78 @@ func (c *BackendCrmProjectController) List() {
|
||||
ownerID := strings.TrimSpace(c.GetString("owner_user_id"))
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
|
||||
// 数据范围:平台/租户管理员可见全部,其他用户仅本人(负责人或创建人)
|
||||
cond = crmApplyOwnerScope(cond, claims)
|
||||
if keyword != "" {
|
||||
kw := orm.NewCondition().
|
||||
Or("project_name__contains", keyword).
|
||||
Or("project_no__contains", keyword).
|
||||
Or("customer_name__contains", keyword).
|
||||
Or("supplier_name__contains", keyword)
|
||||
cond = cond.AndCond(kw)
|
||||
}
|
||||
if status != "" {
|
||||
cond = cond.And("status", status)
|
||||
}
|
||||
if ownerID != "" {
|
||||
cond = cond.And("owner_user_id", ownerID)
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.TenantCrmProject)).SetCond(cond)
|
||||
|
||||
total, _ := qs.Count()
|
||||
// buildCond 构造本次列表的筛选条件。
|
||||
// 「已完成」沉底排列需要按「未完成 / 已完成」两组分别查询,因此这里做成可重复调用的构造方法。
|
||||
buildCond := func() *orm.Condition {
|
||||
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
|
||||
// 数据范围:平台/租户管理员可见全部,其他用户仅本人(负责人或创建人)
|
||||
cond = crmApplyOwnerScope(cond, claims)
|
||||
if keyword != "" {
|
||||
kw := orm.NewCondition().
|
||||
Or("project_name__contains", keyword).
|
||||
Or("project_no__contains", keyword).
|
||||
Or("customer_name__contains", keyword).
|
||||
Or("supplier_name__contains", keyword)
|
||||
cond = cond.AndCond(kw)
|
||||
}
|
||||
if status != "" {
|
||||
cond = cond.And("status", status)
|
||||
}
|
||||
if ownerID != "" {
|
||||
cond = cond.And("owner_user_id", ownerID)
|
||||
}
|
||||
return cond
|
||||
}
|
||||
projectQuery := func(cond *orm.Condition) orm.QuerySeter {
|
||||
return models.Orm.QueryTable(new(models.TenantCrmProject)).SetCond(cond)
|
||||
}
|
||||
// 未完成组:除「已完成」外的全部状态(含历史数据中的空状态),沉底排列时取列表前段
|
||||
unfinishedCond := func() *orm.Condition {
|
||||
return buildCond().AndCond(orm.NewCondition().
|
||||
Or("status__ne", crmProjectStatusDone).
|
||||
Or("status__isnull", true))
|
||||
}
|
||||
|
||||
total, _ := projectQuery(buildCond()).Count()
|
||||
var list []models.TenantCrmProject
|
||||
if total > 0 {
|
||||
_, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
||||
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
|
||||
}
|
||||
} else {
|
||||
// 列表排列:未完成项目在前、「已完成」项目沉底,两组内部仍按新→旧。
|
||||
// 先按两组数量把当前页窗口换算成各组区间,再分别取数按序拼接,保证翻页后整体顺序一致。
|
||||
unfinished, _ := projectQuery(unfinishedCond()).Count()
|
||||
unfinishedTotal := int(unfinished)
|
||||
finishedTotal := int(total) - unfinishedTotal
|
||||
end := offset + pageSize
|
||||
if end > int(total) {
|
||||
end = int(total)
|
||||
}
|
||||
aFrom, aTo := min(offset, unfinishedTotal), min(end, unfinishedTotal)
|
||||
bFrom := min(max(offset-unfinishedTotal, 0), finishedTotal)
|
||||
bTo := min(max(end-unfinishedTotal, 0), finishedTotal)
|
||||
if aTo > aFrom {
|
||||
if _, err := projectQuery(unfinishedCond()).OrderBy("-id").Offset(aFrom).Limit(aTo - aFrom).All(&list); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if bTo > bFrom {
|
||||
var finished []models.TenantCrmProject
|
||||
if _, err := projectQuery(buildCond().And("status", crmProjectStatusDone)).
|
||||
OrderBy("-id").Offset(bFrom).Limit(bTo - bFrom).All(&finished); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
list = append(list, finished...)
|
||||
}
|
||||
}
|
||||
}
|
||||
attachBusinessName(tenantID, list)
|
||||
// 项目起止时间由关联合同汇总(不再依赖手工维护的开始 / 结束日期)
|
||||
@@ -495,6 +568,176 @@ func (c *BackendCrmProjectController) Delete() {
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ========================== 项目联系人(从对应企业通讯录挑选) ==========================
|
||||
|
||||
// contactIDsJSON 把联系人ID数组序列化为 JSON 字符串;空集合返回空串。
|
||||
func contactIDsJSON(ids []uint64) string {
|
||||
if len(ids) == 0 {
|
||||
return ""
|
||||
}
|
||||
b, err := json.Marshal(ids)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// filterCompanyContactIDs 过滤出确实属于指定企业通讯录的联系人ID(防止跨企业/脏ID混入)。
|
||||
// companyID 为空或 ids 为空时直接返回空集合。
|
||||
func filterCompanyContactIDs(tenantID string, companyType string, companyID *uint64, ids []uint64) []uint64 {
|
||||
if companyID == nil || *companyID == 0 || len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
var rows []models.ErpCompanyContact
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpCompanyContact)).
|
||||
Filter("tenant_id", tenantID).
|
||||
Filter("company_type", companyType).
|
||||
Filter("company_id", *companyID).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&rows)
|
||||
allowed := make(map[uint64]bool, len(rows))
|
||||
for _, r := range rows {
|
||||
allowed[r.ID] = true
|
||||
}
|
||||
out := make([]uint64, 0, len(ids))
|
||||
seen := make(map[uint64]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
if id > 0 && allowed[id] && !seen[id] {
|
||||
out = append(out, id)
|
||||
seen[id] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SaveContacts POST /backend/crm/project/:id/contacts
|
||||
// 保存项目联系人:从对应企业通讯录中挑选的联系人ID集合(支持多选)。
|
||||
// - 上游=客户方(customer_id 对应通讯录);
|
||||
// - 下游/中游=对方主体方(supplier_ref_type:1=客户库 2=供应商库,supplier_id 对应通讯录);
|
||||
// - 单一项目(project_type=1)只保留上游集合,下游集合自动清空;
|
||||
// - 传入不属于对应企业的联系人ID会被过滤掉。
|
||||
func (c *BackendCrmProjectController) SaveContacts() {
|
||||
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
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
UpstreamContactIDs []uint64 `json:"upstream_contact_ids"`
|
||||
DownstreamContactIDs []uint64 `json:"downstream_contact_ids"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var proj models.TenantCrmProject
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&proj); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "项目未找到")
|
||||
return
|
||||
}
|
||||
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
|
||||
if !canAccessCrmRecord(claims, proj.OwnerUserID, proj.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 403, 403, "无权操作该项目数据")
|
||||
return
|
||||
}
|
||||
|
||||
pt := proj.ProjectType
|
||||
if pt != 2 && pt != 3 {
|
||||
pt = 1
|
||||
}
|
||||
upIDs := filterCompanyContactIDs(tenantID, "customer", proj.CustomerID, p.UpstreamContactIDs)
|
||||
var downIDs []uint64
|
||||
// 上下游 / 上中游项目:对方主体(下游 / 中游)联系人按 supplier_ref_type 选择对应库
|
||||
if pt != 1 && proj.SupplierID != nil && *proj.SupplierID > 0 {
|
||||
companyType := "supplier"
|
||||
if proj.SupplierRefType == 1 {
|
||||
companyType = "customer"
|
||||
}
|
||||
downIDs = filterCompanyContactIDs(tenantID, companyType, proj.SupplierID, p.DownstreamContactIDs)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", proj.ID).Filter("tenant_id", tenantID).
|
||||
Update(map[string]interface{}{
|
||||
"upstream_contact_ids": contactIDsJSON(upIDs),
|
||||
"downstream_contact_ids": contactIDsJSON(downIDs),
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "保存项目联系人失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 3, proj.ID, "update", "更新项目联系人:"+proj.ProjectName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"upstream_contact_ids": upIDs,
|
||||
"downstream_contact_ids": downIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeStatus POST /backend/crm/project/:id/status
|
||||
// 项目状态流转:1=未开始 2=进行中 3=已完成 4=已暂停 5=异常。
|
||||
// 各状态间可互切;仅更新状态字段,不触碰其他业务字段。
|
||||
func (c *BackendCrmProjectController) ChangeStatus() {
|
||||
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
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.Status < 1 || p.Status > 5 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的状态值")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var proj models.TenantCrmProject
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&proj); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "项目未找到")
|
||||
return
|
||||
}
|
||||
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
|
||||
if !canAccessCrmRecord(claims, proj.OwnerUserID, proj.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 403, 403, "无权操作该项目数据")
|
||||
return
|
||||
}
|
||||
if proj.Status == p.Status {
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": proj.ID, "status": proj.Status})
|
||||
return
|
||||
}
|
||||
if _, err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Update(map[string]interface{}{"status": p.Status, "update_time": time.Now()}); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "状态更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 3, proj.ID, "status",
|
||||
"项目状态流转:"+projectStatusName(proj.Status)+" → "+projectStatusName(p.Status), claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": proj.ID, "status": p.Status})
|
||||
}
|
||||
|
||||
// ========================== 项目文档(OA 文档库绑定) ==========================
|
||||
|
||||
// projectDocItem 项目文档条目:文档库文档 + 所在文件夹名称。
|
||||
|
||||
Reference in New Issue
Block a user