first commit

This commit is contained in:
2026-04-01 15:43:01 +08:00
commit 55fcaf62dc
595 changed files with 70062 additions and 0 deletions

View File

@@ -0,0 +1,311 @@
package service
import (
"bdrp-server/app/main/api/internal/config"
"bdrp-server/app/main/model"
"bytes"
"context"
"database/sql"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"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 {
absStoragePath := determineStoragePath()
return &AuthorizationService{
config: c,
authDocModel: authDocModel,
fileStoragePath: absStoragePath,
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)
}
// ResolveFilePath 根据存储的路径信息解析出本地文件的绝对路径
func (s *AuthorizationService) ResolveFilePath(filePath string, relativePath string) string {
candidates := []string{}
cleanRelative := func(p string) string {
p = strings.TrimPrefix(p, "/")
p = strings.TrimPrefix(p, "\\")
normalized := filepath.ToSlash(p)
if strings.HasPrefix(normalized, "http://") || strings.HasPrefix(normalized, "https://") {
if parsed, err := url.Parse(normalized); err == nil {
normalized = filepath.ToSlash(strings.TrimPrefix(parsed.Path, "/"))
}
}
normalized = strings.TrimPrefix(normalized, "data/authorization_docs/")
normalized = strings.TrimPrefix(normalized, "authorization_docs/")
normalized = strings.TrimPrefix(normalized, "./")
normalized = strings.TrimPrefix(normalized, "../")
return normalized
}
if filePath != "" {
candidates = append(candidates, filePath)
}
if relativePath != "" {
candidates = append(candidates, filepath.Join(s.fileStoragePath, filepath.FromSlash(cleanRelative(relativePath))))
}
if filePath != "" {
cleaned := filepath.Clean(filePath)
if strings.HasPrefix(cleaned, s.fileStoragePath) {
candidates = append(candidates, cleaned)
} else {
trimmed := strings.TrimPrefix(cleaned, "data"+string(os.PathSeparator)+"authorization_docs"+string(os.PathSeparator))
trimmed = strings.TrimPrefix(trimmed, "data/authorization_docs/")
if trimmed != cleaned {
candidates = append(candidates, filepath.Join(s.fileStoragePath, filepath.FromSlash(cleanRelative(trimmed))))
}
if !filepath.IsAbs(cleaned) {
candidates = append(candidates, filepath.Join(s.fileStoragePath, filepath.FromSlash(cleanRelative(cleaned))))
}
}
}
for _, candidate := range candidates {
if candidate == "" {
continue
}
pathCandidate := candidate
if !filepath.IsAbs(pathCandidate) {
if absPath, err := filepath.Abs(pathCandidate); err == nil {
pathCandidate = absPath
}
}
if statErr := checkFileExists(pathCandidate); statErr == nil {
return pathCandidate
}
}
logx.Errorf("授权书文件路径解析失败 filePath=%s fileUrl=%s candidates=%v", filePath, relativePath, candidates)
return ""
}
func determineStoragePath() string {
candidatePaths := []string{
"data/authorization_docs",
"../data/authorization_docs",
"../../data/authorization_docs",
"../../../data/authorization_docs",
}
for _, candidate := range candidatePaths {
absPath, err := filepath.Abs(candidate)
if err != nil {
logx.Errorf("解析授权书存储路径失败: %s, err=%v", candidate, err)
continue
}
if info, err := os.Stat(absPath); err == nil && info.IsDir() {
logx.Infof("授权书存储路径选择: %s", absPath)
return absPath
}
}
// 如果没有现成的目录,使用第一个候选的绝对路径
absPath, err := filepath.Abs(candidatePaths[0])
if err != nil {
logx.Errorf("解析默认授权书存储路径失败,使用相对路径: %s, err=%v", candidatePaths[0], err)
return candidatePaths[0]
}
logx.Infof("授权书存储路径创建: %s", absPath)
return absPath
}
func checkFileExists(path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.IsDir() {
return fmt.Errorf("path %s is directory", path)
}
return nil
}
// 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)
}
// 构建授权书内容(与前端 Authorization.vue 保持一致)
content := fmt.Sprintf(`戎行技术有限公司:
本人(姓名:%s身份证号码%s拟向贵司申请业务贵司需要了解本人相关状况用于查询大数据分析报告因此本人特同意并不可撤销的授权
(一)贵司向依法成立的第三方服务商(包括但不限于天津津北数字产业发展集团有限公司)根据本人提交的信息进行核实;并有权通过前述第三方服务机构查询、使用本人的身份信息、电话号码等,查询本人信息(包括但不限于学历、婚姻、资产状况及对信息主体产生负面影响的不良信息),出具相关报告。
(二)第三方服务商应当在上述处理目的、处理方式和个人信息的种类等范围内处理个人信息。变更原先的处理目的、处理方式的,应当依法重新取得您的同意。
本人在此声明已充分理解上述授权条款含义,知晓并自愿承担上述因收集等本人数据可能会给本人的生活行为(评分)结果产生不利影响,以及该等数据被使用者依法提供给第三方后被他人不当利用的风险,但本人仍同意上述授权。
特别提示:
为了保障您的合法权益,请您务必阅读并充分理解与遵守本授权书;若您不接受本授权书的任何条款,请您立即终止授权。贵司已经对上述事宜及其风险向本人做了充分说明,本人已知晓并同意。
你通过“赤眉”APP或代理商推广查询模式自愿支付相应费用用于购买戎行技术有限公司的大数据报告产品。
你向戎行技术有限公司的支付方式为:戎行技术有限公司及其关联公司的支付宝及微信账户。
本授权书一经本人在网上点击勾选同意即完成签署。本授权书是本人真实意思表示,本人同意承担由此带来的一切法律后果。
授权人:%s
身份证号:%s
签署时间:%s`, name, idCard, 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 ""
}