package service import ( "bytes" "context" "tydata-server/app/main/api/internal/config" "tydata-server/app/main/model" "database/sql" "fmt" "os" "path/filepath" "time" "github.com/jung-kurt/gofpdf" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" ) type AuthorizationService struct { config config.Config authDocModel model.AuthorizationDocumentModel fileStoragePath string fileBaseURL string } // NewAuthorizationService 创建授权书服务实例 func NewAuthorizationService(c config.Config, authDocModel model.AuthorizationDocumentModel) *AuthorizationService { return &AuthorizationService{ config: c, authDocModel: authDocModel, fileStoragePath: "data/authorization_docs", // 使用相对路径,兼容开发环境 fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取 } } // GenerateAuthorizationDocument 生成授权书PDF func (s *AuthorizationService) GenerateAuthorizationDocument( ctx context.Context, userID int64, orderID int64, queryID int64, userInfo map[string]interface{}, ) (*model.AuthorizationDocument, error) { // 1. 生成PDF内容 pdfBytes, err := s.generatePDFContent(userInfo) if err != nil { return nil, errors.Wrapf(err, "生成PDF内容失败") } // 2. 创建文件存储目录 year := time.Now().Format("2006") month := time.Now().Format("01") dirPath := filepath.Join(s.fileStoragePath, year, month) if err := os.MkdirAll(dirPath, 0755); err != nil { return nil, errors.Wrapf(err, "创建存储目录失败: %s", dirPath) } // 3. 生成文件名和路径 fileName := fmt.Sprintf("auth_%d_%d_%s.pdf", userID, orderID, time.Now().Format("20060102_150405")) filePath := filepath.Join(dirPath, fileName) // 只存储相对路径,不包含域名 relativePath := fmt.Sprintf("%s/%s/%s", year, month, fileName) // 4. 保存PDF文件 if err := os.WriteFile(filePath, pdfBytes, 0644); err != nil { return nil, errors.Wrapf(err, "保存PDF文件失败: %s", filePath) } // 5. 保存到数据库 authDoc := &model.AuthorizationDocument{ UserId: userID, OrderId: orderID, QueryId: queryID, FileName: fileName, FilePath: filePath, FileUrl: relativePath, // 只存储相对路径 FileSize: int64(len(pdfBytes)), FileType: "pdf", Status: "active", ExpireTime: sql.NullTime{Valid: false}, // 永久保留,不设置过期时间 } result, err := s.authDocModel.Insert(ctx, nil, authDoc) if err != nil { // 如果数据库保存失败,删除已创建的文件 os.Remove(filePath) return nil, errors.Wrapf(err, "保存授权书记录失败") } authDoc.Id, _ = result.LastInsertId() logx.Infof("授权书生成成功: userID=%d, orderID=%d, filePath=%s", userID, orderID, filePath) return authDoc, nil } // GetFullFileURL 获取完整的文件访问URL func (s *AuthorizationService) GetFullFileURL(relativePath string) string { if relativePath == "" { return "" } return fmt.Sprintf("%s/%s", s.fileBaseURL, relativePath) } // generatePDFContent 生成PDF内容 func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{}) ([]byte, error) { // 创建PDF文档 pdf := gofpdf.New("P", "mm", "A4", "") pdf.AddPage() // 添加中文字体支持 - 参考imageService的路径处理方式 fontPaths := []string{ "static/SIMHEI.TTF", // 相对于工作目录的路径(与imageService一致) "/app/static/SIMHEI.TTF", // Docker容器内的字体文件 "app/main/api/static/SIMHEI.TTF", // 开发环境备用路径 } // 尝试添加字体 fontAdded := false for _, fontPath := range fontPaths { if _, err := os.Stat(fontPath); err == nil { pdf.AddUTF8Font("ChineseFont", "", fontPath) fontAdded = true logx.Infof("成功加载字体: %s", fontPath) break } else { logx.Debugf("字体文件不存在: %s, 错误: %v", fontPath, err) } } // 如果没有找到字体文件,使用默认字体,并记录警告 if !fontAdded { pdf.SetFont("Arial", "", 12) logx.Errorf("未找到中文字体文件,使用默认Arial字体,可能无法正确显示中文") } else { // 设置默认字体 pdf.SetFont("ChineseFont", "", 12) } // 获取用户信息 name := getUserInfoString(userInfo, "name") idCard := getUserInfoString(userInfo, "id_card") // 生成当前日期 currentDate := time.Now().Format("2006年1月2日") // 设置标题样式 - 大字体、居中 if fontAdded { pdf.SetFont("ChineseFont", "", 20) // 使用20号字体 } else { pdf.SetFont("Arial", "", 20) } pdf.CellFormat(0, 15, "授权书", "", 1, "C", false, 0, "") // 添加空行 pdf.Ln(5) // 设置正文样式 - 正常字体 if fontAdded { pdf.SetFont("ChineseFont", "", 12) } else { pdf.SetFont("Arial", "", 12) } // 构建授权书内容(去掉标题部分) content := fmt.Sprintf(`海南天远大数据科技有限公司: 本人%s拟向贵司申请大数据分析报告查询业务,贵司需要了解本人相关状况,用于查询大数据分析报告,因此本人同意向贵司提供本人的姓名和手机号等个人信息,并同意贵司向第三方(包括但不限于西部数据交易有限公司)传送上述信息。第三方将使用上述信息核实信息真实情况,查询信用记录,并生成报告。 授权内容如下: 贵司向依法成立的第三方服务商(包括但不限于西部数据交易有限公司)根据本人提交的信息进行核实,并有权通过前述第三方服务机构查询、使用本人的身份信息、设备信息、运营商信息等,查询本人信息(包括但不限于学历、婚姻、资产状况及对信息主体产生负面影响的不良信息),出具相关报告。 依法成立的第三方服务商查询或核实、搜集、保存、处理、共享、使用(含合法业务应用)本人相关数据,且不再另行告知本人,但法律、法规、监管政策禁止的除外。 本人授权有效期为自授权之日起 1个月。本授权为不可撤销授权,但法律法规另有规定的除外。 用户声明与承诺: 本人在授权签署前,已通过实名认证及动态验证码验证(或其他身份验证手段),确认本授权行为为本人真实意思表示,平台已履行身份验证义务。 本人在此声明已充分理解上述授权条款含义,知晓并自愿承担因授权数据使用可能带来的后果,包括但不限于影响个人信用评分、生活行为等。本人确认授权范围内的相关信息由本人提供并真实有效。 若用户冒名签署或提供虚假信息,由用户自行承担全部法律责任,平台不承担任何后果。 特别提示: 本产品所有数据均来自第三方。可能部分数据未公开、数据更新延迟或信息受到限制,贵司不对数据的准确性、真实性、完整性做任何承诺。用户需根据实际情况,结合报告内容自行判断与决策。 本产品仅供用户本人查询或被授权查询。除非用户取得合法授权,用户不得利用本产品查询他人信息。用户因未获得合法授权而擅自查询他人信息所产生的任何后果,由用户自行承担责任。 本授权书涉及对本人敏感信息(包括但不限于婚姻状态、资产状况等)的查询与使用。本人已充分知晓相关信息的敏感性,并明确同意贵司及其合作方依据授权范围使用相关信息。 平台声明:本授权书涉及的信息核实及查询结果由第三方服务商提供,平台不对数据的准确性、完整性、实时性承担责任;用户根据报告所作决策的风险由用户自行承担,平台对此不承担法律责任。 本授权书中涉及的数据查询和报告生成由依法成立的第三方服务商提供。若因第三方行为导致数据错误或损失,用户应向第三方主张权利,平台不承担相关责任。 附加说明: 本人在授权的相关数据将依据法律法规及贵司内部数据管理规范妥善存储,存储期限为法律要求的最短必要时间。超过存储期限或在数据使用目的达成后,贵司将对相关数据进行销毁或匿名化处理。 本人有权随时撤回本授权书中的授权,但撤回前的授权行为及其法律后果仍具有法律效力。若需撤回授权,本人可通过贵司官方渠道提交书面申请,贵司将在收到申请后依法停止对本人数据的使用。 你通过"天远数据",自愿支付相应费用,用于购买海南天远大数据科技有限公司的大数据报告产品。如若对产品内容存在异议,可通过邮箱admin@iieeii.com或APP"联系客服"按钮进行反馈,贵司将在收到异议之日起20日内进行核查和处理,并将结果答复。 你向海南天远大数据科技有限公司的支付方式为:海南天远大数据科技有限公司及其经官方授权的相关企业的支付宝账户。 争议解决机制: 若因本授权书引发争议,双方应友好协商解决;协商不成的,双方同意将争议提交至授权书签署地(海南省)有管辖权的人民法院解决。 签署方式的法律效力声明: 本授权书通过用户在线勾选、电子签名或其他网络签署方式完成,与手写签名具有同等法律效力。平台已通过技术手段保存签署过程的完整记录,作为用户真实意思表示的证据。 本授权书于 %s 生效。 授权人:%s 身份证号:%s 签署时间:%s`, name, currentDate, name, idCard, currentDate) // 将内容写入PDF pdf.MultiCell(0, 6, content, "", "", false) // 生成PDF字节数组 var buf bytes.Buffer err := pdf.Output(&buf) if err != nil { return nil, errors.Wrapf(err, "生成PDF字节数组失败") } return buf.Bytes(), nil } // getUserInfoString 安全获取用户信息字符串 func getUserInfoString(userInfo map[string]interface{}, key string) string { if value, exists := userInfo[key]; exists { if str, ok := value.(string); ok { return str } } return "" }