diff --git a/data/满意度调成报告模板.pdf b/data/满意度调成报告模板.pdf new file mode 100644 index 00000000..e7f44b98 Binary files /dev/null and b/data/满意度调成报告模板.pdf differ diff --git a/pkg/service/bundle/model/questionnaireSurvey.go b/pkg/service/bundle/model/questionnaireSurvey.go new file mode 100644 index 00000000..7fc621e9 --- /dev/null +++ b/pkg/service/bundle/model/questionnaireSurvey.go @@ -0,0 +1,35 @@ +package model + +type QuestionnairePDFData struct { + // 基本信息 + CustomerNum string `json:"customerNum"` + CustomerName string `json:"customerName"` + BundleName string `json:"bundleName"` + BundleStartDate string `json:"bundleStartDate"` + BundleEndDate string `json:"bundleEndDate"` + VideoNum string `json:"videoNum"` + AccountNum string `json:"accountNum"` + ImagesNum string `json:"imagesNum"` + DataAnalysisNum string `json:"dataAnalysisNum"` + CompetitiveNum string `json:"competitiveNum"` + ValueAddVideoNum string `json:"valueAddVideoNum"` + + // 评分(1-5) + Score1 int `json:"score1"` + Score2 int `json:"score2"` + Score3 int `json:"score3"` + Score4 int `json:"score4"` + Score5 int `json:"score5"` + Score6 int `json:"score6"` + Score7 int `json:"score7"` + + // 意见 + Opinion1 string `json:"opinion1"` + Opinion2 string `json:"opinion2"` + Opinion3 string `json:"opinion3"` + + // 提交信息 + Submitter string `json:"submitter"` + SubmissionDate string `json:"submissionDate"` + Address string `json:"address"` +} diff --git a/pkg/service/bundle/questionnaireSurvey.go b/pkg/service/bundle/questionnaireSurvey.go index 4b5ed242..98274531 100644 --- a/pkg/service/bundle/questionnaireSurvey.go +++ b/pkg/service/bundle/questionnaireSurvey.go @@ -1,8 +1,15 @@ package bundle import ( + "errors" + "fmt" "fonchain-fiee/api/bundle" "fonchain-fiee/pkg/service" + "fonchain-fiee/pkg/service/bundle/model" + "fonchain-fiee/pkg/service/upload" + "fonchain-fiee/pkg/utils" + "os" + "time" "github.com/gin-gonic/gin" ) @@ -40,5 +47,54 @@ func QuestionnaireSurveyCreate(c *gin.Context) { service.Error(c, err) return } + address, err := utils.ReverseGeo("31.367784", "120.647276", "ZhCN") + if err != nil { + service.Error(c, errors.New("获取地址失败")) + return + } + surveyInfo, err := service.BundleProvider.GetQuestionnaireSurveyInfo(c, &bundle.GetQuestionnaireSurveyInfoRequest{UserTel: req.UserTel}) + if err != nil { + service.Error(c, err) + return + } + templateDir := "./data/满意度调成报告模板.pdf" + outputPath := "./data/" + req.UserTel + time.Now().Format("20060102150405") + ".pdf" + err = utils.QuestionnaireSurveyPDF(templateDir, outputPath, &model.QuestionnairePDFData{ + //CustomerNum: surveyInfo.BundleInfo., + CustomerName: surveyInfo.UserName, + BundleName: surveyInfo.BundleInfo.BundleName, + BundleStartDate: surveyInfo.BundleInfo.StartAt, + BundleEndDate: surveyInfo.BundleInfo.ExpiredAt, + VideoNum: string(surveyInfo.BundleInfo.BundleVideoNumber), + AccountNum: string(surveyInfo.BundleInfo.BundleAccountNumber), + ImagesNum: string(surveyInfo.BundleInfo.BundleImageNumber), + DataAnalysisNum: string(surveyInfo.BundleInfo.BundleDataNumber), + CompetitiveNum: string(surveyInfo.BundleInfo.BundleCompetitiveNumber), + ValueAddVideoNum: string(surveyInfo.BundleInfo.IncreaseVideoNumber), + Score1: int(req.SurveyAnswer.BundleAccountScore), + Score2: int(req.SurveyAnswer.BundleAccountScore), + Score3: int(req.SurveyAnswer.BundleImageScore), + Score4: int(req.SurveyAnswer.BundleDataScore), + Score5: int(req.SurveyAnswer.IncreaseVideoScore), + Score6: int(req.SurveyAnswer.ServiceResponseSpeed), + Score7: int(req.SurveyAnswer.ServiceStaffProfessionalism), + Opinion1: req.SurveyFeedback.MeritsReview, + Opinion2: req.SurveyFeedback.SuggestionsorImprovements, + Opinion3: req.SurveyFeedback.AdditionalComments, + Submitter: surveyInfo.UserName, + SubmissionDate: time.Now().Format(time.DateOnly), + Address: address, + }) + if err != nil { + service.Error(c, err) + return + } + os.Remove(outputPath) + outputUrl, ossErr := upload.PutBos(outputPath, upload.PdfType, true) + if ossErr != nil { + service.Error(c, err) + return + } + fmt.Println(outputUrl) service.Success(c, req) } diff --git a/pkg/utils/map.go b/pkg/utils/map.go new file mode 100644 index 00000000..356fa2d3 --- /dev/null +++ b/pkg/utils/map.go @@ -0,0 +1,88 @@ +package utils + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" +) + +type ReverseGeocodingReq struct { + Ak string `json:"ak"` + Coordtype string `json:"coordtype"` + RetCoordtype string `json:"retCoordtype"` + Location string `json:"location"` + Output string `json:"output"` + Language string `json:"language"` +} + +type ReverseGeocodingRes struct { + Status int `json:"status"` + Result struct { + Location struct { + Lng float64 `json:"lng"` + Lat float64 `json:"lat"` + } `json:"location"` + FormattedAddress string `json:"formatted_address"` + Edz struct { + Name string `json:"name"` + } `json:"edz"` + Business string `json:"business"` + AddressComponent struct { + Country string `json:"country"` + CountryCodeIso string `json:"country_code_iso"` + CountryCodeIso2 string `json:"country_code_iso2"` + CountryCode int `json:"country_code"` + Province string `json:"province"` + City string `json:"city"` + CityLevel int `json:"city_level"` + District string `json:"district"` + Town string `json:"town"` + TownCode string `json:"town_code"` + Distance string `json:"distance"` + Direction string `json:"direction"` + Adcode string `json:"adcode"` + Street string `json:"street"` + StreetNumber string `json:"street_number"` + } `json:"addressComponent"` + } `json:"result"` +} + +// ReverseGeo 经纬度逆编码 +func ReverseGeo(longitude, latitude string, language string) (address string, err error) { + var reverseGeocodingReq ReverseGeocodingReq + + reverseGeocodingReq.Ak = "3bAjKGA0pv7qvszGe98RsVZ04Ob5r4ZZ" + reverseGeocodingReq.Coordtype = "gcj02ll" + reverseGeocodingReq.Output = "json" + reverseGeocodingReq.RetCoordtype = "gcj02ll" + reverseGeocodingReq.Location = strings.Join([]string{latitude, longitude}, ",") + reverseGeocodingReq.Language = language + + url := "https://api.map.baidu.com/reverse_geocoding/v3/?ak=" + reverseGeocodingReq.Ak + "&output=" + reverseGeocodingReq.Output + "&coordtype=" + reverseGeocodingReq.Coordtype + "&location=" + reverseGeocodingReq.Location + "&ret_coordtype=" + reverseGeocodingReq.RetCoordtype + "&language=" + reverseGeocodingReq.Language + resp, err := http.Get(url) + + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var results ReverseGeocodingRes + err = json.Unmarshal(body, &results) + + if err != nil { + return "", err + } + if results.Status != 0 { + address = "未知地址" + return address, err + } + + address = results.Result.FormattedAddress + + return address, nil +} diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index 4b824ea4..af5f4c9b 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -3,6 +3,7 @@ package utils import ( "errors" "fmt" + "fonchain-fiee/pkg/service/bundle/model" "image" "io" "net/http" @@ -10,7 +11,9 @@ import ( "os" "path/filepath" "strings" + "time" "unicode" + "unicode/utf8" "github.com/phpdave11/gofpdf" "github.com/signintech/gopdf" @@ -595,3 +598,273 @@ func ConvertCompetitorReportToText(data CompetitorReportData, isVideo bool) stri return sb.String() } + +//生成问卷调查pdf + +func QuestionnaireSurveyPDF(templatePath, outputPath string, data *model.QuestionnairePDFData) error { + pdf := gopdf.GoPdf{} + pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4}) + + if err := pdf.ImportPagesFromSource(templatePath, "/MediaBox"); err != nil { + return fmt.Errorf("导入模板失败: %w", err) + } + + if err := pdf.AddTTFFont("simfang", "./data/simfang.ttf"); err != nil { + return fmt.Errorf("加载字体失败: %w", err) + } + + if err := pdf.SetFont("simfang", "", 12); err != nil { + return fmt.Errorf("设置字体失败: %w", err) + } + + startTime, err := time.Parse("2006-01-02", data.BundleStartDate) + if err != nil { + return fmt.Errorf("BundleStartDate格式错误: %w", err) + } + endTime, err := time.Parse("2006-01-02", data.BundleEndDate) + if err != nil { + return fmt.Errorf("BundleEndDate格式错误: %w", err) + } + submissionDate, err := time.Parse("2006-01-02", data.SubmissionDate) + if err != nil { + return fmt.Errorf("submissionDate格式错误: %w", err) + } + nowTime := time.Now().Format(time.DateTime) + onePage := 1 + twoPage := 2 + threePage := 3 + fourPage := 4 + + // 第1页:客户基本信息 + pdf.SetPage(onePage) + //姓名 + pdf.SetX(165) + pdf.SetY(420) + pdf.Cell(nil, data.CustomerName) + //套餐名称 + pdf.SetX(205) + pdf.SetY(443) + pdf.Cell(nil, data.BundleName) + //开始日期 + pdf.SetX(205) + pdf.SetY(467) + pdf.Cell(nil, startTime.Format("2006")) + pdf.SetX(260) + pdf.SetY(467) + pdf.Cell(nil, startTime.Format("01")) + pdf.SetX(300) + pdf.SetY(467) + pdf.Cell(nil, startTime.Format("02")) + + //结束日期 + pdf.SetX(350) + pdf.SetY(467) + pdf.Cell(nil, endTime.Format("2006")) + pdf.SetX(398) + pdf.SetY(467) + pdf.Cell(nil, endTime.Format("01")) + pdf.SetX(437) + pdf.SetY(467) + pdf.Cell(nil, endTime.Format("02")) + //视频数 + pdf.SetX(220) + pdf.SetY(583) + pdf.Cell(nil, data.VideoNum) + //"账号数: "+ + pdf.SetX(230) + pdf.SetY(625) + pdf.Cell(nil, data.AccountNum) + // "图文数: "+ + pdf.SetX(253) + pdf.SetY(667) + pdf.Cell(nil, data.ImagesNum) + //"数据分析数: "+ + pdf.SetX(280) + pdf.SetY(727) + pdf.Cell(nil, data.DataAnalysisNum) + // 第1页内容写完后 + if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil { + return err + } + // 第2页:服务数量 + pdf.SetPage(twoPage) + + //"竞品分析数: "+ + pdf.SetX(205) + pdf.SetY(72) + pdf.Cell(nil, data.CompetitiveNum) + //"增值视频数: "+ + pdf.SetX(270) + pdf.SetY(156) + pdf.Cell(nil, data.ValueAddVideoNum) + //"评分1: "+ + pdf.SetX(123) + pdf.SetY(485) + pdf.Cell(nil, scoreStars(data.Score1)) + //"评分2: "+ + pdf.SetX(343) + pdf.SetY(526) + pdf.Cell(nil, scoreStars(data.Score2)) + //"评分3: "+ + pdf.SetX(230) + pdf.SetY(568) + pdf.Cell(nil, scoreStars(data.Score3)) + //"评分4: "+ + pdf.SetX(362) + pdf.SetY(610) + pdf.Cell(nil, scoreStars(data.Score4)) + //"评分5: "+ + pdf.SetX(220) + pdf.SetY(652) + pdf.Cell(nil, scoreStars(data.Score5)) + //"评分6: "+ + pdf.SetX(164) + pdf.SetY(694) + pdf.Cell(nil, scoreStars(data.Score6)) + //"评分7: "+ + pdf.SetX(197) + pdf.SetY(735) + pdf.Cell(nil, scoreStars(data.Score7)) + //水印 + if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil { + return err + } + // 第3页:评分与意见 + pdf.SetPage(threePage) + // Opinion 超过100字符时自动换行,每行约20个中文字符,行高18pt + drawWrappedText(&pdf, data.Opinion1, 90, 145, 24, 34) + drawWrappedText(&pdf, data.Opinion2, 90, 377, 24, 34) + drawWrappedText(&pdf, data.Opinion3, 90, 574, 24, 34) + //水印 + if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil { + return err + } + + // 第4页:提交信息 + pdf.SetPage(fourPage) + //"提交人: "+ + pdf.SetX(135) + pdf.SetY(103) + pdf.Cell(nil, data.Submitter) + //提交时间: " + pdf.SetX(148) + pdf.SetY(128) + pdf.Cell(nil, submissionDate.Format("2006")) + pdf.SetX(207) + pdf.SetY(128) + pdf.Cell(nil, submissionDate.Format("01")) + pdf.SetX(260) + pdf.SetY(128) + pdf.Cell(nil, submissionDate.Format("02")) + //水印 + if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil { + return err + } + if err := pdf.WritePdf(outputPath); err != nil { + return fmt.Errorf("写入PDF失败: %w", err) + } + return nil +} + +// addWatermark 在当前页叠加浅色斜向双行水印,并在结束后恢复样式 +func addWatermark(pdf *gopdf.GoPdf, text string) error { + const normalFontSize = 12 + const watermarkFontSize = 22 + const lineHeight = 32.0 // 水印行间距 + + // 设置水印样式 + pdf.SetGrayFill(0.85) + if err := pdf.SetFont("simfang", "", watermarkFontSize); err != nil { + return fmt.Errorf("设置水印字体失败: %w", err) + } + + // 按换行拆分,逐行绘制(Cell 不负责自动换行) + lines := strings.Split(text, "\n") + if len(lines) == 0 { + lines = []string{text} + } + + drawBlock := func(x, y, cx, cy float64) { + pdf.Rotate(35, cx, cy) + for i, line := range lines { + pdf.SetX(x) + pdf.SetY(y + float64(i)*lineHeight) + pdf.Cell(nil, line) + } + pdf.RotateReset() + } + + // 两处重复水印 + drawBlock(90, 420, 300, 420) + drawBlock(130, 620, 300, 420) + + // 恢复样式,避免影响后续正文 + pdf.SetGrayFill(0) + if err := pdf.SetFont("simfang", "", normalFontSize); err != nil { + return fmt.Errorf("恢复字体失败: %w", err) + } + return nil +} + +// drawWrappedText 在 PDF 上绘制自动换行的文字 +// pdf: GoPdf 实例, text: 文字内容, startX/startY: 起始坐标 +// maxWidth: 最大宽度(pt), lineHeight: 行高(pt), charsPerLine: 每行最多字符数(按中文字符计) +func drawWrappedText(pdf *gopdf.GoPdf, text string, startX, startY, lineHeight float64, charsPerLine int) { + runes := []rune(text) + total := len(runes) + if total == 0 { + return + } + + lineStart := 0 + currentLine := 0 + + for lineStart < total { + end := lineStart + charsPerLine + if end > total { + end = total + } + // 计算实际宽度以决定换行点(按字节估算:ASCII=0.5中文字符宽) + charCount := 0 + splitAt := lineStart + for i := lineStart; i < total; i++ { + r := runes[i] + runeBytes := utf8.RuneLen(r) + if runeBytes > 1 { + charCount += 2 // 中文等宽字符算2个单位 + } else { + charCount += 1 // ASCII 算1个单位 + } + if charCount > charsPerLine*2 { + break + } + splitAt = i + 1 + } + if splitAt == lineStart { + splitAt = lineStart + 1 + } + + line := string(runes[lineStart:splitAt]) + pdf.SetX(startX) + pdf.SetY(startY + float64(currentLine)*lineHeight) + pdf.Cell(nil, line) + + lineStart = splitAt + currentLine++ + } +} + +func scoreStars(score int) string { + switch { + case score <= 1: + return "★☆☆☆☆" + case score == 2: + return "★★☆☆☆" + case score == 3: + return "★★★☆☆" + case score == 4: + return "★★★★☆" + default: + return "★★★★★" + } +}