package service import ( "context" "fmt" "strconv" "sync" "time" contractdao "dashoo.cn/opms_parent/app/dao/contract" opsdevdao "dashoo.cn/opms_parent/app/dao/opsdev" projdao "dashoo.cn/opms_parent/app/dao/proj" contractmodel "dashoo.cn/opms_parent/app/model/contract" workflowmodel "dashoo.cn/opms_parent/app/model/workflow" "dashoo.cn/opms_parent/app/service" "dashoo.cn/opms_libary/micro_srv" "dashoo.cn/opms_libary/myerrors" "dashoo.cn/opms_libary/plugin/dingtalk/message" "github.com/gogf/gf/database/gdb" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/util/gconv" ) // ctrContractAdvanceService 合同提前执行申请服务 type ctrContractAdvanceService struct { *service.ContextService Dao *contractdao.CtrContractAdvanceDao } var ctrContractAdvanceMtx sync.Mutex // NewCtrContractAdvanceService 初始化服务 func NewCtrContractAdvanceService(ctx context.Context) (*ctrContractAdvanceService, error) { svc := &ctrContractAdvanceService{} var err error if svc.ContextService, err = svc.Init(ctx); err != nil { return nil, err } svc.Dao = contractdao.NewCtrContractAdvanceDao(svc.Tenant) return svc, nil } // GetList 分页查询 func (s *ctrContractAdvanceService) GetList(ctx context.Context, req *contractmodel.CtrContractAdvanceSearchReq) (total int, list []*contractmodel.CtrContractAdvanceRsp, err error) { db := s.Dao.As("a") // 拼接查询条件 if req.SearchText != "" { likestr := fmt.Sprintf("%%%s%%", req.SearchText) db = db.Where("(a.advance_code LIKE ? OR a.advance_name LIKE ? OR a.cust_name LIKE ? OR a.nbo_name LIKE ?)", likestr, likestr, likestr, likestr) } if req.AdvanceCode != "" { likestr := fmt.Sprintf("%%%s%%", req.AdvanceCode) db = db.Where("a.advance_code LIKE ?", likestr) } if req.AdvanceName != "" { likestr := fmt.Sprintf("%%%s%%", req.AdvanceName) db = db.Where("a.advance_name LIKE ?", likestr) } if req.CustId != 0 { db = db.Where("a.cust_id = ?", req.CustId) } if req.CustName != "" { likestr := fmt.Sprintf("%%%s%%", req.CustName) db = db.Where("a.cust_name LIKE ?", likestr) } if req.NboId != 0 { db = db.Where("a.nbo_id = ?", req.NboId) } if req.ApproStatus != "" { db = db.Where("a.appro_status = ?", req.ApproStatus) } if req.ProductLine != "" { db = db.Where("a.product_line = ?", req.ProductLine) } if req.InchargeId != 0 { db = db.Where("a.incharge_id = ?", req.InchargeId) } if req.ContractId != 0 { db = db.Where("a.contract_id = ?", req.ContractId) } // 统计总行数 total, err = db.Count() if err != nil { g.Log().Error(err) return 0, nil, myerrors.DbError("获取提前执行申请总行数失败") } // 分页、排序、查询 var entityList []*contractmodel.CtrContractAdvance err = db.Page(req.GetPage()).OrderDesc("a.id").Scan(&entityList) if err != nil { g.Log().Error(err) return 0, nil, myerrors.DbError("查询提前执行申请列表失败") } // 转换为响应结构体 if err = gconv.Structs(entityList, &list); err != nil { g.Log().Error(err) return 0, nil, myerrors.DbError("数据转换失败") } return total, list, nil } // GetById 根据ID查询单条 func (s *ctrContractAdvanceService) GetById(id int) (*contractmodel.CtrContractAdvanceRsp, error) { entity, err := s.Dao.WherePri(id).One() if err != nil { g.Log().Error(err) return nil, myerrors.DbError("查询提前执行申请数据失败") } if entity == nil { return nil, myerrors.TipsError("提前执行申请数据不存在") } var rsp contractmodel.CtrContractAdvanceRsp if err := gconv.Struct(entity, &rsp); err != nil { g.Log().Error(err) return nil, myerrors.DbError("数据转换失败") } return &rsp, nil } // Create 新增 func (s *ctrContractAdvanceService) Create(req *contractmodel.CtrContractAdvanceAddReq) error { // 生成申请编号: TQ + 年月日 + 4位序号 advanceCode := s.generateAdvanceCode() data := gconv.Map(req) data["advanceCode"] = advanceCode data["approStatus"] = contractmodel.AdvanceStatusDraft data["tenantId"] = s.Tenant // 补齐审计字段 service.SetCreatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName()) // 获取项目信息填充客户信息 if req.NboId != 0 { projBusinessDao := projdao.NewProjBusinessDao(s.Tenant) proj, err := projBusinessDao.Where("id = ?", req.NboId).One() if err != nil { return myerrors.DbError("查询项目信息失败") } if proj != nil { data["custId"] = proj.CustId data["custName"] = proj.CustName data["nboName"] = proj.NboName data["productLine"] = proj.ProductLine data["isBig"] = proj.IsBig data["custProvinceId"] = proj.CustProvinceId data["custProvince"] = proj.CustProvince data["custCityId"] = proj.CustCityId data["custCity"] = proj.CustCity } } // 事务控制 return s.Dao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { _, err := s.Dao.TX(tx).Insert(data) if err != nil { g.Log().Error(err) return myerrors.DbError("新增提前执行申请失败") } // 写业务动态日志 s.CreateDynamic(s.Tenant, 0, "创建提前执行申请", fmt.Sprintf("创建提前执行申请:%s", advanceCode), s.GetCxtUserId(), s.GetCxtUserName()) return nil }) } // UpdateById 根据ID更新 func (s *ctrContractAdvanceService) UpdateById(req *contractmodel.CtrContractAdvanceUpdateReq) error { // 校验数据是否存在 entity, err := s.Dao.WherePri(req.Id).One() if err != nil { g.Log().Error(err) return myerrors.DbError("查询提前执行申请数据失败") } if entity == nil { return myerrors.TipsError("提前执行申请数据不存在") } // 校验状态:只有待提交(10)或审核拒绝(40)状态可以编辑 if entity.ApproStatus != contractmodel.AdvanceStatusDraft && entity.ApproStatus != contractmodel.AdvanceStatusRejected { return myerrors.TipsError("当前状态不可编辑") } data := gconv.Map(req) delete(data, "id") // 移除ID字段 // 获取项目信息更新客户信息 if req.NboId != 0 && req.NboId != entity.NboId { projBusinessDao := projdao.NewProjBusinessDao(s.Tenant) proj, err := projBusinessDao.Where("id = ?", req.NboId).One() if err != nil { return myerrors.DbError("查询项目信息失败") } if proj != nil { data["custId"] = proj.CustId data["custName"] = proj.CustName data["nboName"] = proj.NboName data["productLine"] = proj.ProductLine data["isBig"] = proj.IsBig data["custProvinceId"] = proj.CustProvinceId data["custProvince"] = proj.CustProvince data["custCityId"] = proj.CustCityId data["custCity"] = proj.CustCity } } // 补齐审计字段 service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName()) // 更新操作,排除不可改字段 _, err = s.Dao.FieldsEx(service.UpdateFieldEx...).WherePri(req.Id).Data(data).Update() if err != nil { g.Log().Error(err) return myerrors.DbError("更新提前执行申请失败") } // 写业务动态日志 s.CreateDynamic(s.Tenant, req.Id, "编辑提前执行申请", fmt.Sprintf("编辑提前执行申请:%s", entity.AdvanceCode), s.GetCxtUserId(), s.GetCxtUserName()) return nil } // DeleteById 根据ID单条删除 func (s *ctrContractAdvanceService) DeleteById(id int) error { entity, err := s.Dao.WherePri(id).One() if err != nil { g.Log().Error(err) return myerrors.DbError("查询提前执行申请数据失败") } if entity == nil { return myerrors.TipsError("提前执行申请数据不存在") } // 校验状态:只有待提交(10)或审核拒绝(40)状态可以删除 if entity.ApproStatus != contractmodel.AdvanceStatusDraft && entity.ApproStatus != contractmodel.AdvanceStatusRejected { return myerrors.TipsError("当前状态不可删除") } _, err = s.Dao.WherePri(id).Delete() if err != nil { g.Log().Error(err) return myerrors.DbError("删除提前执行申请失败") } // 写业务动态日志 s.CreateDynamic(s.Tenant, id, "删除提前执行申请", fmt.Sprintf("删除提前执行申请:%s", entity.AdvanceCode), s.GetCxtUserId(), s.GetCxtUserName()) return nil } // DeleteByIds 根据ID批量删除 func (s *ctrContractAdvanceService) DeleteByIds(ids []int64) error { if len(ids) == 0 { return myerrors.TipsError("请选择需要删除的数据") } // 查询所有记录校验状态 var entities []*contractmodel.CtrContractAdvance err := s.Dao.WhereIn(s.Dao.Columns.Id, ids).Scan(&entities) if err != nil { g.Log().Error(err) return myerrors.DbError("查询提前执行申请数据失败") } for _, entity := range entities { if entity == nil { continue } if entity.ApproStatus != contractmodel.AdvanceStatusDraft && entity.ApproStatus != contractmodel.AdvanceStatusRejected { return myerrors.TipsError(fmt.Sprintf("申请[%s]当前状态不可删除", entity.AdvanceCode)) } } _, err = s.Dao.WhereIn(s.Dao.Columns.Id, ids).Delete() if err != nil { g.Log().Error(err) return myerrors.DbError("批量删除提前执行申请失败") } // 写业务动态日志 s.CreateDynamic(s.Tenant, 0, "批量删除提前执行申请", fmt.Sprintf("批量删除提前执行申请,数量:%d", len(ids)), s.GetCxtUserId(), s.GetCxtUserName()) return nil } // Commit 提交审批 func (s *ctrContractAdvanceService) Commit(req *contractmodel.CtrContractAdvanceCommitReq) error { entity, err := s.Dao.WherePri(req.Id).One() if err != nil { g.Log().Error(err) return myerrors.DbError("查询提前执行申请数据失败") } if entity == nil { return myerrors.TipsError("提前执行申请数据不存在") } // 校验状态:只有待提交(10)或审核拒绝(40)状态可以提交 if entity.ApproStatus != contractmodel.AdvanceStatusDraft && entity.ApproStatus != contractmodel.AdvanceStatusRejected { return myerrors.TipsError("当前状态不可提交审批") } // 模拟审批:不调用钉钉接口,直接更新状态为审核中 // 如需启用钉钉审批,取消下面注释并添加必要的 import /* workflowService, err := workflowsrv.NewFlowService(s.Ctx) if err != nil { return err } bizCode := strconv.Itoa(entity.Id) processCode := "PROC-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" _, err = workflowService.StartProcessInstance(bizCode, workflowmodel.ContractAdvance, "", &workflow.StartProcessInstanceRequest{ ProcessCode: &processCode, FormComponentValues: []*workflow.StartProcessInstanceRequestFormComponentValues{ { Name: utils.String("申请编号"), Value: utils.String(entity.AdvanceCode), }, { Name: utils.String("项目名称"), Value: utils.String(entity.AdvanceName), }, { Name: utils.String("客户名称"), Value: utils.String(entity.CustName), }, { Name: utils.String("预估金额"), Value: utils.String(strconv.FormatFloat(entity.EstimateAmount, 'f', 2, 64)), }, { Name: utils.String("提前执行原因"), Value: utils.String(entity.AdvanceReason), }, }, }) if err != nil { return err } */ // 更新状态为审核中 _, err = s.Dao.WherePri(req.Id).Data(g.Map{ "appro_status": contractmodel.AdvanceStatusApproving, }).Update() if err != nil { g.Log().Error(err) return myerrors.DbError("更新审批状态失败") } // 写业务动态日志 s.CreateDynamic(s.Tenant, req.Id, "提交审批", fmt.Sprintf("提交提前执行申请审批:%s", entity.AdvanceCode), s.GetCxtUserId(), s.GetCxtUserName()) return nil } // Approve 审批通过 func (s *ctrContractAdvanceService) Approve(req *contractmodel.IdRequiredReq) error { entity, err := s.Dao.WherePri(req.Id).One() if err != nil { g.Log().Error(err) return myerrors.DbError("查询提前执行申请数据失败") } if entity == nil { return myerrors.TipsError("提前执行申请数据不存在") } // 校验状态:只有审核中(20)状态可以通过 if entity.ApproStatus != contractmodel.AdvanceStatusApproving { return myerrors.TipsError("当前状态不可审批通过") } return s.Dao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { // 更新状态为审核通过 _, err = s.Dao.TX(tx).WherePri(req.Id).Data(g.Map{ "appro_status": contractmodel.AdvanceStatusApproved, }).Update() if err != nil { g.Log().Error(err) return myerrors.DbError("更新审批状态失败") } // 创建交付项目 err = CreateProjectByAdvance(tx, s.Tenant, entity, s.GetCxtUserId(), s.GetCxtUserName()) if err != nil { g.Log().Error(err) return myerrors.DbError("创建交付项目失败") } return nil }) } // Reject 审批拒绝 func (s *ctrContractAdvanceService) Reject(req *contractmodel.IdRequiredReq) error { entity, err := s.Dao.WherePri(req.Id).One() if err != nil { g.Log().Error(err) return myerrors.DbError("查询提前执行申请数据失败") } if entity == nil { return myerrors.TipsError("提前执行申请数据不存在") } // 校验状态:只有审核中(20)状态可以拒绝 if entity.ApproStatus != contractmodel.AdvanceStatusApproving { return myerrors.TipsError("当前状态不可审批拒绝") } // 更新状态为审核拒绝 _, err = s.Dao.WherePri(req.Id).Data(g.Map{ "appro_status": contractmodel.AdvanceStatusRejected, }).Update() if err != nil { g.Log().Error(err) return myerrors.DbError("更新审批状态失败") } return nil } // ConvertToContract 转为正式合同 func (s *ctrContractAdvanceService) ConvertToContract(req *contractmodel.CtrContractAdvanceConvertReq) error { entity, err := s.Dao.WherePri(req.Id).One() if err != nil { g.Log().Error(err) return myerrors.DbError("查询提前执行申请数据失败") } if entity == nil { return myerrors.TipsError("提前执行申请数据不存在") } // 校验状态:只有审核通过(30)状态可以转正式合同 if entity.ApproStatus != contractmodel.AdvanceStatusApproved { return myerrors.TipsError("只有审核通过的申请才能转为正式合同") } // 校验是否已转合同 if entity.ContractId != 0 { return myerrors.TipsError("该申请已转为正式合同") } return s.Dao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { // 更新提前执行申请为已转合同状态 _, err = s.Dao.TX(tx).WherePri(req.Id).Data(g.Map{ "contract_id": req.ContractId, "contract_code": req.ContractCode, "convert_time": gtime.Now(), "convert_by": s.GetCxtUserId(), "convert_name": s.GetCxtUserName(), "appro_status": contractmodel.AdvanceStatusConverted, }).Update() if err != nil { g.Log().Error(err) return myerrors.DbError("更新提前执行申请状态失败") } // 更新交付项目关联的合同ID,并将申请ID记录到attribute1 deliveryProjectDao := opsdevdao.NewOpsDeliveryProjectDao(s.Tenant) _, err = deliveryProjectDao.TX(tx).Where("contract_id = ?", req.ContractId).Data(g.Map{ "contract_id": req.ContractId, "attribute1": strconv.Itoa(req.Id), // 记录申请ID到attribute1 "updated_time": gtime.Now(), }).Update() if err != nil { g.Log().Error(err) return myerrors.DbError("更新交付项目合同关联失败") } return nil }) } // generateAdvanceCode 生成申请编号 TQ + 年月日 + 4位序号 func (s *ctrContractAdvanceService) generateAdvanceCode() string { dateStr := time.Now().Format("20060102") seq := s.getNextSequence(dateStr) return fmt.Sprintf("TQ%s%04d", dateStr, seq) } // getNextSequence 获取当日序号 func (s *ctrContractAdvanceService) getNextSequence(dateStr string) int { count, err := s.Dao.WhereLike("advance_code", fmt.Sprintf("TQ%s%%", dateStr)).Count() if err != nil { g.Log().Error(err) return 1 } return count + 1 } // CreateDynamic 创建业务动态 func (s *ctrContractAdvanceService) CreateDynamic(tenant string, businessId int, opnType, opnContent string, userId int, userName string) { dynamic := map[string]interface{}{ "business_id": businessId, "business_type": "contract_advance", "opn_type": opnType, "opn_content": opnContent, "opn_people_id": userId, "opn_people": userName, "opn_date": gtime.Now(), "created_time": gtime.Now(), } _, err := s.Dao.DB.Model("business_dynamic").Data(dynamic).Insert() if err != nil { g.Log().Error("创建业务动态失败:", err) } } // AdvanceApplyApproval 审批回调处理 func AdvanceApplyApproval(ctx context.Context, flow *workflowmodel.PlatWorkflow, msg *message.MixMessage) error { ctrContractAdvanceMtx.Lock() defer ctrContractAdvanceMtx.Unlock() tenant, err := micro_srv.GetTenant(ctx) if err != nil { return fmt.Errorf("获取租户码异常:%s", err.Error()) } advanceDao := contractdao.NewCtrContractAdvanceDao(tenant) advanceId, err := strconv.Atoi(flow.BizCode) if err != nil { return fmt.Errorf("提前执行申请审批 bizCode 不合法:%s Id: %d", flow.BizCode, flow.Id) } advance, err := advanceDao.Where("id = ?", advanceId).One() if err != nil { return err } if advance == nil { return fmt.Errorf("提前执行申请不存在:%s Id: %d", flow.BizCode, flow.Id) } // 校验回调类型和结果 if msg.ProcessType != "finish" && msg.ProcessType != "terminate" { return fmt.Errorf("无法识别的 ProcessType:%s", msg.ProcessType) } if msg.Result != "agree" && msg.Result != "refuse" && msg.Result != "" { return fmt.Errorf("无法识别的 Result:%s", msg.Result) } // 确定新状态 var status string if msg.ProcessType == "terminate" { status = contractmodel.AdvanceStatusCancelled } else { if msg.Result == "agree" { status = contractmodel.AdvanceStatusApproved } else { status = contractmodel.AdvanceStatusRejected } } // 更新状态 _, err = advanceDao.Where("id = ?", advanceId).Data(g.Map{ "appro_status": status, }).Update() if err != nil { return err } // 如果审批通过,自动创建交付项目(参考合同审批通过逻辑) if status == contractmodel.AdvanceStatusApproved { err = advanceDao.DB.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { // 创建交付项目,使用申请创建人作为项目创建人 err := CreateProjectByAdvance(tx, tenant, advance, advance.CreatedBy, advance.CreatedName) if err != nil { return err } return nil }) if err != nil { return err } } return nil } // CreateProjectByAdvance 根据提前执行申请创建交付项目 func CreateProjectByAdvance(tx *gdb.TX, tenant string, advance *contractmodel.CtrContractAdvance, createdBy int, createdName string) error { if advance == nil { return fmt.Errorf("提前执行申请信息不能为空") } projectDao := opsdevdao.NewOpsDeliveryProjectDao(tenant) // 幂等创建:检查是否已存在 count, err := projectDao.TX(tx).Where("attribute1 = ? AND deleted_time IS NULL", strconv.Itoa(advance.Id)).Count() if err != nil { return err } if count > 0 { return nil } // 创建交付项目 data := g.Map{ "project_name": advance.AdvanceName, "project_status": "10", "contract_id": advance.Id, // 申请ID作为临时合同ID "contract_no": advance.AdvanceCode, // 申请编号作为临时合同编号 "cust_id": strconv.Itoa(advance.CustId), "cust_name": advance.CustName, "product_line": advance.ProductLine, "sales_user_id": advance.InchargeId, "sales_user_name": advance.InchargeName, "sales_region_id": advance.CustCityId, "delivery_node": "05", "attribute1": strconv.Itoa(advance.Id), // 记录提前执行申请ID "created_by": createdBy, "created_name": createdName, "created_time": gtime.Now(), "updated_time": gtime.Now(), } _, err = projectDao.TX(tx).Data(data).Insert() return err } // UpdateAdvanceContract 更新提前执行申请的正式合同信息 func (s *ctrContractAdvanceService) UpdateAdvanceContract(req *contractmodel.CtrContractAdvanceConvertReq) error { if req.Id <= 0 { return myerrors.TipsError("提前执行申请ID不能为空") } if req.ContractId <= 0 { return myerrors.TipsError("合同ID不能为空") } if req.ContractCode == "" { return myerrors.TipsError("合同编号不能为空") } return s.Dao.DB.Transaction(s.Ctx, func(ctx context.Context, tx *gdb.TX) error { // 1. 更新提前执行申请表 _, err := tx.Update("ctr_contract_advance", g.Map{ "contract_id": req.ContractId, "contract_code": req.ContractCode, "appro_status": "60", // 已转正式合同 }, "id = ?", req.Id) if err != nil { return err } // 2. 更新交付项目表 projectDao := opsdevdao.NewOpsDeliveryProjectDao(s.Tenant) _, err = projectDao.TX(tx).Data(g.Map{ "contract_id": req.ContractId, "contract_no": req.ContractCode, }).Where("attribute1 = ?", strconv.Itoa(req.Id)).Update() return err }) }