package order import ( "app.yhyue.com/moapp/jybase/common" "app.yhyue.com/moapp/jybase/date" "app.yhyue.com/moapp/jybase/redis" "context" "crypto/md5" "encoding/hex" "fmt" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" "github.com/lukasjarosch/go-docx" "github.com/pkg/errors" cncap "github.com/rosbit/cn-capitalizer" "jyOrderManager/internal/jyutil" "jyOrderManager/internal/model" "log" "math/rand" "os/exec" "path/filepath" "runtime" "strconv" "strings" "time" ) const ( bigMemberContractNoFlag = "JYDZ05" subVipContractNoFlag = "JYDZ06" /* 1 超级订阅:https://www.kdocs.cn/l/cpqTn293crus 2 大会员(购买主体“企业”,非单省版):https://www.kdocs.cn/l/cv9HKtmoO3tj 3 大会员(购买主体“企业”,单省版):https://www.kdocs.cn/l/cm6OWJbTgmhE 4 大会员(购买主体“个人”,非单省版-有子账号):https://www.kdocs.cn/l/cgtyiJGraYmd 5 大会员(购买主体“个人”,非单省版-无子账号):https://www.kdocs.cn/l/clxt7vdFZ5Ea 6 大会员(购买主体“个人”,单省版-无子账号):https://www.kdocs.cn/l/cscOCLIvvG1a; 7 大会员(购买主体“个人”,单省版-有子账号):https://www.kdocs.cn/l/cfPHDdQsROBh */ ContractTemplate_SUBVIP = "剑鱼标讯超级订阅产品服务协议书" ContractTemplate_MEMBER_ENT_NOTSINGLE = "剑鱼标讯大会员产品服务协议书-(企业-非单省版)" ContractTemplate_MEMBER_ENT_SINGLE = "剑鱼标讯大会员产品服务协议书-(企业-单省版)" ContractTemplate_MEMBER_PERSON_NOTSINGLE_ACCOUNT = "剑鱼标讯大会员产品服务协议书-(个人-非单省版-有子账号)" ContractTemplate_MEMBER_PERSON_NOTSINGLE_NOTACCOUNT = "剑鱼标讯大会员产品服务协议书-(个人-非单省版-无子账号)" ContractTemplate_MEMBER_PERSON_SINGLE_NOTACCOUNT = "剑鱼标讯大会员产品服务协议书-(个人-单省版-无子账号)" ContractTemplate_MEMBER_PERSON_SINGLE_ACCOUNT = "剑鱼标讯大会员产品服务协议书-(个人-单省版-有子账号)" ) func GetContractPdf(ctx context.Context, orderCode, userName string) (interface{}, error) { detail, orderQueryErr := func() (map[string]interface{}, error) { if num, _ := g.DB().GetCount(ctx, "SELECT count(*) FROM jy_order_detail WHERE order_code=?", orderCode); num != 1 { return nil, fmt.Errorf("当前订单不支持合同下载") } orderRes, err := g.DB().GetOne(ctx, "SELECT c.filter,c.service_type,a.audit_status,c.product_type,a.vip_type,a.salesperson,a.buy_subject,a.signing_subject,b.contract_status,b.seal_type,b.partyA_type,b.partyA_name,b.partyA_person,b.partyA_tel,b.partyA_address,b.partyB_person,b.remark,b.contract_money FROM dataexport_order a INNER JOIN jy_order_detail c ON a.order_code=c.order_code INNER JOIN contract b ON a.order_code=b.order_code WHERE a.order_code=? ", orderCode) if err != nil { return nil, err } if orderRes.IsEmpty() { return nil, fmt.Errorf("未找到订单") } return orderRes.Map(), nil }() if orderQueryErr != nil { return nil, gerror.Wrapf(orderQueryErr, "GetContractPdf orderQueryErr %s订单查询异常 %v\n", orderCode, orderQueryErr) } filterMap := *common.ObjToMap(detail["filter"]) contractPass := func() error { /* 判断是否符合合同 (1)订单审核状态是“已通过”; (2)签约主体为“北京剑鱼信息技术有限公司”,注:如签约主体为拓普则线下生成合同; (3)协议状态为“签协议”(“已签协议”文案修改为“签协议”,涉及:创建订单、订单审核、订单详情) (4)产品类型是“超级订阅”(且付费类型为“购买”、“续费”),或产品类型是“大会员”且会员套餐为“商机版2.0”、“专家版2.0”(且服务类型为“新购服务”、“延长服务”),注:超级订阅/大会员升级和其他产品类型,线下生成合同。 */ if common.IntAll(detail["audit_status"]) != 3 { return fmt.Errorf("订单未审核通过") } if common.ObjToString(detail["signing_subject"]) != "h01" { return fmt.Errorf("当前签约主体不支持生成电子合同") } if common.IntAll(detail["contract_status"]) != 1 { return fmt.Errorf("协议状态不需要") } var service_type = gconv.Int(detail["service_type"]) switch common.ObjToString(detail["product_type"]) { case "VIP订阅": // 'vip订阅-0:购买 1:续费 2:升级',3试用 if !(service_type == 1 || service_type == 4) { return fmt.Errorf("超级订阅当前订单不支持生成电子合同") } case "大会员": level := common.IntAll(filterMap["comboId"]) if !(service_type == 1 || service_type == 4) && (level == 6 || level == 7) { return fmt.Errorf("大会员当前订单不支持生成电子合同") } default: return fmt.Errorf("当前订单类型不支持生成电子合同") } return nil }() if contractPass != nil { log.Printf("GetContractPdf contractPass %s合同校验异常 %v\n", orderCode, contractPass) return nil, contractPass } // contractDetail, err := func() (*ContractDetail, error) { remark := common.ObjToString(detail["remark"]) ctd := &ContractDetail{ ContractFromInfo: &ContractFromInfo{ SealType: common.IntAll(detail["seal_type"]), PartyAType: common.IntAll(detail["partyA_type"]), PartyAName: common.ObjToString(detail["partyA_name"]), PartyAPerson: common.ObjToString(detail["partyA_person"]), PartyAContact: common.ObjToString(detail["partyA_tel"]), PartyAAddress: common.ObjToString(detail["partyA_address"]), PartyBPerson: common.ObjToString(detail["partyB_person"]), Remark: common.ObjToString(common.If(remark != "", remark, "无")), }, Amount: common.Float64All(detail["contract_money"]), } buy_subject := common.IntAll(detail["buy_subject"]) switch common.ObjToString(detail["product_type"]) { case "VIP订阅": var filter model.VipCycleFilter if err := gconv.Struct(filterMap, &filter); err != nil { return nil, gerror.Wrapf(err, "格式化超级订阅内容异常") } //时间 switch filter.BuyType { //1天 2月 3年 4季度 case 1: ctd.ServiceTime = fmt.Sprintf("%d天(自然日)", filter.BuyCycle) case 2: ctd.ServiceTime = fmt.Sprintf("%d个月", filter.BuyCycle) case 3: ctd.ServiceTime = fmt.Sprintf("%d个月", filter.BuyCycle*12) case 4: ctd.ServiceTime = fmt.Sprintf("%d个月", filter.BuyCycle*3) default: return nil, fmt.Errorf("未知周期单位") } switch filter.GiftType { //1天 2月 3年 4季度 case 1: ctd.ServiceTime = fmt.Sprintf("送%d天(自然日)", filter.GiftCycle) case 2: ctd.ServiceTime = fmt.Sprintf("送%d个月", filter.GiftCycle) case 3: ctd.ServiceTime = fmt.Sprintf("送%d个月", filter.GiftCycle*12) case 4: ctd.ServiceTime = fmt.Sprintf("送%d个月", filter.GiftCycle*3) } if num := filter.BuyAccountCount; num > 0 { ctd.Service = fmt.Sprintf("省份版超级订阅(%d个省)", num) } else { ctd.Service = "全国版超级订阅" } if buy_subject == 1 { ctd.AccountNum = 1 } else { ctd.AccountNum = 1 + filter.BuyAccountCount + filter.GiftAccountCount } ctd.ContractFlag = subVipContractNoFlag ctd.ContractTemplate = ContractTemplate_SUBVIP case "大会员": level := common.IntAll(filterMap["comboId"]) isSINGLE := common.IntAll(filterMap["areaCount"]) == 1 //是单省版 hasACCOUNT := common.IntAll(filterMap["free_sub_num"])+common.IntAll(filterMap["pay_sub_num"]) >= 1 //有子账号 if buy_subject == 2 { ctd.AccountNum = common.IntAll(detail["buy_count"]) } if level == 6 { if isSINGLE { ctd.Service = "大会员商机版2.0(单省版)" } else { ctd.Service = "大会员商机版2.0" } } else if level == 7 { ctd.Service = "大会员专家版2.0" } cycleType := common.IntAll(filterMap["cycleType"]) if cycleType == 1 { ctd.ServiceTime = fmt.Sprintf("%d天(自然日)", common.IntAll(filterMap["cycle"])) } else { ctd.ServiceTime = fmt.Sprintf("%d个月", common.IntAll(filterMap["cycle"])) } if buy_subject == 1 { //个人 ctd.AccountNum = common.IntAll(filterMap["free_sub_num"]) + common.IntAll(filterMap["pay_sub_num"]) + 1 if isSINGLE { if hasACCOUNT { ctd.ContractTemplate = ContractTemplate_MEMBER_PERSON_SINGLE_ACCOUNT } else { ctd.ContractTemplate = ContractTemplate_MEMBER_PERSON_SINGLE_NOTACCOUNT } } else { if hasACCOUNT { ctd.ContractTemplate = ContractTemplate_MEMBER_PERSON_NOTSINGLE_ACCOUNT } else { ctd.ContractTemplate = ContractTemplate_MEMBER_PERSON_NOTSINGLE_NOTACCOUNT } } } else { //企业 if isSINGLE { ctd.ContractTemplate = ContractTemplate_MEMBER_ENT_SINGLE } else { ctd.ContractTemplate = ContractTemplate_MEMBER_ENT_NOTSINGLE } } ctd.ContractFlag = bigMemberContractNoFlag } return ctd, nil }() if err != nil { return nil, gerror.Wrapf(err, "获取合同内容出错") } filePath, getFilePathErr := func() (string, error) { md5Str := contractDetail.GetMd5() contract, err := g.DB().GetOne(ctx, "SELECT file_path FROM contract_pdf WHERE order_code=? and md5=?", orderCode, md5Str) if err != nil { return "", gerror.Wrapf(err, "获取pdf电子合同异常") } if contract.IsEmpty() { now := time.Now() docxAbsolutePath, err := contractDetail.getDocxFile(now) if err != nil { return "", errors.Wrap(err, "生成电子合同docx文件异常") } if err = contractDetail.convertWordToPDF(docxAbsolutePath); err != nil { return "", errors.Wrap(err, "生成电子合同pdf文件异常") } // 获取访问路径 var ( pdfFilePath = jyutil.GetRequestPath(strings.Replace(docxAbsolutePath, ".docx", ".pdf", 1)) contractCode = contractDetail.GetContractNoStr(now) ) if _, err := g.DB().Save(ctx, "contract_pdf", g.Map{ "order_code": orderCode, "code": contractCode, "md5": md5Str, "create_person": userName, "file_path": pdfFilePath, "create_time": now.Format(date.Date_Full_Layout), }); err != nil { return "", errors.Wrapf(err, "保存pdf内容异常") } if _, err := g.DB().Update(ctx, "contract", g.Map{ "contract_code": contractCode, }, "order_code=?", orderCode); err != nil { return "", errors.Wrapf(err, "更新合同异常") } return pdfFilePath, nil } return gconv.String(contract.Map()["file_path"]), nil }() if getFilePathErr != nil { log.Printf("GetContractPdf getFilePathErr %s合同生成异常 %v\n", orderCode, getFilePathErr) return nil, getFilePathErr } return filePath, nil } type ContractFromInfo struct { SealType int `json:"seal_type" doc:"协议类型 1:有电子章 2:无电子章"` PartyAType int `json:"partyAType" doc:"甲方类型 1:个人,2:企业"` PartyAName string `json:"partyAName" doc:"甲方名称"` PartyAPerson string `json:"partyAPerson" doc:"甲方联系人"` PartyAContact string `json:"partyAContact" doc:"甲方联系方式"` PartyAAddress string `json:"partyAAddresst" doc:"甲方联系地址"` PartyBPerson string `json:"partyBPerson" doc:"乙方联系人"` Remark string `json:"remark" doc:"协议备注"` } type ContractDetail struct { *ContractFromInfo Service string `json:"service" doc:"服务内容"` AccountNum int `json:"accountNum" doc:"账户个数"` ServiceTime string `json:"serviceTime" doc:"时长"` Amount float64 `json:"amount" doc:"金额"` ContractNo string `json:"contractNo" doc:"协议编号"` ContractFlag string `json:"contractFlag" doc:"协议编号标识"` ContractTemplate string `json:"contractTemplate" doc:"模版名字"` } // GetMd5 计算合同md5 (甲方+甲方类型+联系人+联系方式+地址+备注+乙方联系人+是否有电子章+服务内容+金额+账户个数+时长) func (detail *ContractDetail) GetMd5() string { hasher := md5.New() hasher.Write([]byte(fmt.Sprintf("%s_%d_%s_%s_%s_%s_%s_%d_%s_%d_%d_%s", detail.PartyAName, detail.PartyAType, detail.PartyAPerson, detail.PartyAContact, detail.PartyAAddress, detail.Remark, detail.PartyBPerson, detail.SealType, detail.Service, detail.Amount, detail.AccountNum, detail.ServiceTime))) hashInBytes := hasher.Sum(nil) return hex.EncodeToString(hashInBytes) } func (detail *ContractDetail) getDocxFile(createTime time.Time) (filePath string, err error) { replaceMap := docx.PlaceholderMap{ "PartyA.Name": detail.PartyAName, //甲方名字 {PartyA.Name} "PartyA.Person": detail.PartyAPerson, //甲方联系人 {PartyA.Person} "PartyA.Tel": detail.PartyAContact, //甲方联系方式 {PartyA.Tel} "PartyA.Addr": detail.PartyAAddress, //甲方地址 {PartyA.Addr} "Contract.No": detail.GetContractNoStr(createTime), //合同编号 {Contract.No} "Contract.Service.Detail": detail.Service, //会员服务 {Contract.Service.Detail} "Contract.Service.Time": detail.ServiceTime, //服务时长 {Contract.Service.Time} "Contract.Account.Num": detail.AccountNum, //账号个数 {Contract.Account.Num} "Contract.Remark": detail.Remark, //合同备注 {Contract.Remark} "Contract.Amount": strconv.FormatFloat(detail.Amount/100, 'f', -1, 64), //合同金额 {Contract.Amount} "Contract.Amount.Font": cncap.CapitalizeCurrency(detail.Amount / 100.0), //合同大写金额 {Contract.Amount.Font} "PartyB.Person": detail.PartyBPerson, //乙方联系人 {PartyB.Person} "PartyB.Date.Y": createTime.Year(), //乙方日期-年 {PartyB.Date.Year} "PartyB.Date.M": fmt.Sprintf("%02d", int(createTime.Month())), //乙方日期-月 {PartyB.Date.Month} "PartyB.Date.D": fmt.Sprintf("%02d", createTime.Day()), //乙方日期-日 {PartyB.Date.Day} } if detail.PartyAType == 2 { replaceMap["PartyA.EndName"] = detail.PartyAName } else { replaceMap["PartyA.EndName"] = "" } numPageEmptyRows := 0 longNum := len([]rune(fmt.Sprintf("%s%s%d%s", detail.Service, detail.ServiceTime, detail.AccountNum, detail.Remark))) if longNum <= 30 { //备注过短时,公章上移 18+13 numPageEmptyRows = 3 } else if longNum > 30 && longNum <= 88 { numPageEmptyRows = 2 } else if longNum > 88 && longNum <= 146 { numPageEmptyRows = 1 } replaceMap["PageEmptyRows"] = strings.Repeat(" ", numPageEmptyRows*180) //计算第一页空行数,把落款固定到最下部 // read and parse the template docx doc, err := docx.Open(fmt.Sprintf("web/static/contract/%s.docx", detail.ContractTemplate)) if err != nil { return "", errors.Wrap(err, "docx文件模版文件异常") } // replace the keys with values from replaceMap err = doc.ReplaceAll(replaceMap) if err != nil { return "", errors.Wrap(err, "docx文件模版文件参数替换异常") } fileAbsolutePath := jyutil.GetFilePath("docx", "contract") // write out a new file err = doc.WriteToFile(fileAbsolutePath) if err != nil { return "", errors.Wrap(err, "docx文件生成异常") } return fileAbsolutePath, nil } // GetContractNoStr 获取生成协议编号 func (detail *ContractDetail) GetContractNoStr(createTime time.Time) string { if detail.ContractNo != "" { return detail.ContractNo } dataStr := createTime.Format(date.Date_yyyyMMdd) key := fmt.Sprintf("%s_%s", detail.ContractFlag, dataStr) detail.ContractNo = fmt.Sprintf("%s【%s】第%03d号", detail.ContractFlag, dataStr, redis.Incr("newother", key)) return detail.ContractNo } // convertWordToPDF word转pdf func (detail *ContractDetail) convertWordToPDF(inputDocxPath string) error { var commandName string var commandArg string switch runtime.GOOS { case "windows": commandName = "cmd" commandArg = "/c" case "linux", "darwin": commandName = "sh" commandArg = "-c" default: return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) } fileDir := filepath.Dir(inputDocxPath) fileFullName := filepath.Base(inputDocxPath) fileName := strings.TrimSuffix(fileFullName, filepath.Ext(fileFullName)) pdfFileFullPath := fmt.Sprintf("%s/%s.pdf", fileDir, fileName) //生成pdf路径 pngFileFullPath := fmt.Sprintf("%s/%s.png", fileDir, fileName) //生成图片路径 pngsMatchPath := fmt.Sprintf("%s/%s-*.png", fileDir, fileName) //生成图片路径 // 一、soffice 生成pdf文件 createPdfFile := exec.Command(commandName, commandArg, fmt.Sprintf("soffice --headless --invisible --convert-to pdf %s --outdir %s", inputDocxPath, fileDir)) if _, err := createPdfFile.CombinedOutput(); err != nil { fmt.Printf(fmt.Sprintf("soffice --headless --invisible --convert-to pdf %s --outdir %s", inputDocxPath, fileDir)) return errors.Wrap(err, "Command execution createPdfFile failed:") } // 二、convert pdf内容转为图片 var hasSeal bool = detail.SealType == 1 if hasSeal { createPngFile := exec.Command(commandName, commandArg, fmt.Sprintf("convert -density 300x300 -units pixelsperinch %s -background white -alpha remove -alpha off -quality 100 -antialias -resize 100%% %s ", pdfFileFullPath, pngFileFullPath)) if _, err := createPngFile.CombinedOutput(); err != nil { fmt.Printf("convert -density 300x300 -units pixelsperinch %s -background white -alpha remove -alpha off -quality 100 -antialias -resize 100%% %s ", pdfFileFullPath, pngFileFullPath) return errors.Wrap(err, "Command execution createPngFile failed:") } //每张图片 cmd := exec.Command("sh", "-c", fmt.Sprintf("ls %s", pngsMatchPath)) out, err := cmd.CombinedOutput() if err != nil || len(out) == 0 { return errors.Wrap(err, "Command execution ls pngs failed:") } for index, fileName := range strings.Split(string(out), "\n") { if fileTmpPath := strings.TrimSpace(fileName); fileTmpPath != "" { var x, y int64 rand.Seed(time.Now().UnixNano()) // 随机盖章位置 生成一个随机整数 if index == 0 { //首页需要盖着日期 x = 680 + rand.Int63n(10) y = 130 + rand.Int63n(30) //if len([]rune(fmt.Sprintf("%s%s%d%s", detail.Service, detail.ServiceTime, detail.AccountNum, detail.Remark))) <= 32 { //备注过短时,公章上移 // y = y + 50 //} } else { x = 300 + rand.Int63n(200) y = 500 + rand.Int63n(200) } exeSeal := exec.Command(commandName, commandArg, fmt.Sprintf("convert %s \\( %s -resize 50%% \\) -gravity southeast -geometry +%d+%d -composite %s", fileTmpPath, "web/static/contract/z.png", x, y, fileTmpPath)) if _, err := exeSeal.CombinedOutput(); err != nil { return errors.Wrap(err, "Command execution exeSeal failed:") } } } createPngPdfFile := exec.Command(commandName, commandArg, fmt.Sprintf("convert %s %s", pngsMatchPath, pdfFileFullPath)) if _, err := createPngPdfFile.CombinedOutput(); err != nil { fmt.Printf("convert %s %s", pngsMatchPath, pdfFileFullPath) return errors.Wrap(err, "Command execution createPngFile failed:") } } else { createPngPdfFile := exec.Command(commandName, commandArg, fmt.Sprintf("convert -density 300x300 -units pixelsperinch %s -background white -alpha remove -alpha off -quality 100 -antialias -resize 100%% %s && convert %s %s", pdfFileFullPath, pngFileFullPath, pngsMatchPath, pdfFileFullPath)) if _, err := createPngPdfFile.CombinedOutput(); err != nil { fmt.Println(fmt.Sprintf("convert -density 300x300 -units pixelsperinch %s -background white -alpha remove -alpha off -quality 100 -antialias -resize 100%% %s && convert %s %s", pdfFileFullPath, pngFileFullPath, pngsMatchPath, pdfFileFullPath)) return errors.Wrap(err, "Command execution createPngFile failed:") } } // 三、清除多余文件 clearFileCmd := exec.Command(commandName, commandArg, fmt.Sprintf("chmod -x %s && rm -rf %s %s", pngsMatchPath, pngsMatchPath, inputDocxPath)) if err := clearFileCmd.Run(); err != nil { return errors.Wrap(err, "Command execution clearFileCmd failed:") } return nil }