150 lines
3.5 KiB
Go
150 lines
3.5 KiB
Go
package hyfile
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
var invalidFilenameChars = regexp.MustCompile(`[\\/:*?"<>|]`)
|
||
|
||
// PDFNameInput 监管处方 PDF 文件名组成字段。
|
||
type PDFNameInput struct {
|
||
PatientName string
|
||
StoreName string
|
||
PrescriptionNo string
|
||
At time.Time
|
||
}
|
||
|
||
// BuildPDFFilename 生成 {患者}{诊所} {处方号} {时间}.pdf
|
||
func BuildPDFFilename(in PDFNameInput) string {
|
||
patient := sanitizeFilename(strings.TrimSpace(in.PatientName))
|
||
if patient == "" || patient == "未知患者" {
|
||
patient = "未知患者"
|
||
}
|
||
store := sanitizeFilename(strings.TrimSpace(in.StoreName))
|
||
prefix := patient
|
||
if store != "" && store != "未知患者" {
|
||
prefix = patient + store
|
||
}
|
||
if prefix == "" {
|
||
prefix = "未知患者"
|
||
}
|
||
no := sanitizeFilename(strings.TrimSpace(in.PrescriptionNo))
|
||
if no == "" {
|
||
no = "未知处方号"
|
||
}
|
||
at := in.At
|
||
if at.IsZero() {
|
||
at = time.Now()
|
||
}
|
||
stamp := at.Format("2006-01-02 15-04-05")
|
||
base := fmt.Sprintf("%s %s %s.pdf", prefix, no, stamp)
|
||
base = sanitizeFilename(strings.TrimSuffix(base, ".pdf")) + ".pdf"
|
||
return base
|
||
}
|
||
|
||
// SavePDFLocal 将 PDF 保存到 {程序目录}/pdf/{YYYY-MM-DD}/{患者}{诊所} {处方号} {时间}.pdf
|
||
func SavePDFLocal(pdf []byte, in PDFNameInput) (string, error) {
|
||
if len(pdf) == 0 {
|
||
return "", fmt.Errorf("empty pdf bytes")
|
||
}
|
||
|
||
root := resolveProgramDir()
|
||
at := in.At
|
||
if at.IsZero() {
|
||
at = time.Now()
|
||
}
|
||
dateDir := at.Format("2006-01-02")
|
||
baseName := BuildPDFFilename(in)
|
||
|
||
dir := filepath.Join(root, "pdf", dateDir)
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
return "", fmt.Errorf("mkdir %s: %w", dir, err)
|
||
}
|
||
|
||
fullPath := filepath.Join(dir, baseName)
|
||
fullPath, err := uniquePath(fullPath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if err := os.WriteFile(fullPath, pdf, 0o644); err != nil {
|
||
return "", fmt.Errorf("write pdf: %w", err)
|
||
}
|
||
|
||
return fullPath, nil
|
||
}
|
||
|
||
// ProgramDir 返回 PDF 落盘根目录(与 SavePDFLocal 一致)。
|
||
func ProgramDir() string {
|
||
return resolveProgramDir()
|
||
}
|
||
|
||
// resolveProgramDir 返回可执行文件所在目录;go run 时使用当前工作目录。
|
||
func resolveProgramDir() string {
|
||
if exe, err := os.Executable(); err == nil {
|
||
dir := filepath.Dir(exe)
|
||
if strings.Contains(dir, "go-build") {
|
||
if wd, err := os.Getwd(); err == nil && wd != "" {
|
||
return wd
|
||
}
|
||
}
|
||
return dir
|
||
}
|
||
if wd, err := os.Getwd(); err == nil && wd != "" {
|
||
return wd
|
||
}
|
||
return "."
|
||
}
|
||
|
||
func sanitizeFilename(name string) string {
|
||
s := invalidFilenameChars.ReplaceAllString(name, "_")
|
||
s = strings.TrimSpace(s)
|
||
if s == "" {
|
||
return ""
|
||
}
|
||
return s
|
||
}
|
||
|
||
// ParseTimeString 解析处方开具时间等字符串(供 pdf-test 与 sync 共用)。
|
||
func ParseTimeString(s string) time.Time {
|
||
s = strings.TrimSpace(s)
|
||
if s == "" {
|
||
return time.Time{}
|
||
}
|
||
layouts := []string{
|
||
"2006-01-02 15:04:05",
|
||
"2006-01-02 15:04",
|
||
time.RFC3339,
|
||
"2006-01-02",
|
||
}
|
||
for _, layout := range layouts {
|
||
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
|
||
return t
|
||
}
|
||
}
|
||
return time.Time{}
|
||
}
|
||
|
||
func uniquePath(path string) (string, error) {
|
||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||
return path, nil
|
||
} else if err != nil {
|
||
return "", err
|
||
}
|
||
ext := filepath.Ext(path)
|
||
base := strings.TrimSuffix(path, ext)
|
||
for i := 2; i < 1000; i++ {
|
||
candidate := fmt.Sprintf("%s_%d%s", base, i, ext)
|
||
if _, err := os.Stat(candidate); os.IsNotExist(err) {
|
||
return candidate, nil
|
||
} else if err != nil {
|
||
return "", err
|
||
}
|
||
}
|
||
return "", fmt.Errorf("too many duplicate filenames for %s", path)
|
||
}
|