feat(mapping): 完成 PHIS 健康档案转换(T-212)

This commit is contained in:
ila
2026-07-16 00:53:16 +08:00
parent d385d6dd3b
commit 8a5550057c
20 changed files with 1078 additions and 124 deletions
+2 -7
View File
@@ -3,14 +3,9 @@ package mapping
import (
"crypto/sha1"
"encoding/hex"
"strings"
)
func GenerateCheckID(sourceSystem, dataType, sourceRecordID, version string) string {
parts := []string{sourceSystem, dataType, sourceRecordID}
if version != "" {
parts = append(parts, version)
}
sum := sha1.Sum([]byte(strings.Join(parts, "|")))
func GenerateCheckID(sourceSystem, dataType, sourceRecordID string) string {
sum := sha1.Sum([]byte(sourceSystem + "|" + dataType + "|" + sourceRecordID))
return hex.EncodeToString(sum[:])[:20]
}
+392 -65
View File
@@ -1,48 +1,101 @@
package mapping
import "chis_osi/contract"
import (
"strconv"
"strings"
"time"
"unicode/utf8"
"chis_osi/contract"
"chis_osi/source"
)
type PHISHealthRecord struct {
SourceSystem string
SourceRecordID string
UpdatedAt string
IDCard string
PersonName string
SexCode string
Birthday string
MobileNumber string
RegionCode string
RegionName string
ManaDoctorID string
ManaDoctorName string
ManaUnitID string
ManaUnitName string
NationCode string
EducationCode string
WorkCode string
MaritalStatusCode string
BloodTypeCode string
RhBloodCode string
InsuranceCode string
SourceSystem string
SourceRecordID string
UpdatedAt string
IDCard string
PersonName string
SexCode string
Birthday string
MobileNumber string
RegionCode string
RegionName string
ManaDoctorID string
ManaDoctorName string
ManaUnitID string
ManaUnitName string
NationCode string
EducationCode string
WorkCode string
MaritalStatusCode string
BloodTypeCode string
RhBloodCode string
InsuranceCode string
WorkPlace string
Contact string
ContactPhone string
RegisteredPermanent string
Address string
AddressNumber string
HomePlace string
HomePlaceNumber string
CardType string
IsFillSHHJ string
CreateDate string
DrugAllergy string
ExposureHistory string
FamilyFather string
FamilyMother string
FamilySiblings string
FamilyChildren string
GeneticDisease string
Disability string
PastDiseaseCode string
PastSurgeryCode string
PastTraumaCode string
PastTransfusionCode string
CookAirTool string
FuelType string
WaterSourceCode string
Washroom string
LivestockColumn string
}
type MappedHealthRecord struct {
CheckID string
IDCard string
PersonName string
SexCode string
Birthday string
MobileNumber string
RegionCode string
ManaDoctorID string
ManaUnitID string
NationCode string
EducationCode string
WorkCode string
MaritalStatusCode string
BloodTypeCode string
RhBloodCode string
InsuranceCode string
CheckID string
IDCard string
PersonName string
SexCode string
Birthday string
MobileNumber string
RegionCode string
ManaDoctorID string
ManaUnitID string
NationCode string
EducationCode string
WorkCode string
MaritalStatusCode string
BloodTypeCode string
RhBloodCode string
InsuranceCode string
WorkPlace string
Contact string
ContactPhone string
RegisteredPermanent string
Address string
AddressNumber string
HomePlace string
HomePlaceNumber string
CardType string
IsFillSHHJ string
CreateDate string
PastHistory *contract.PastHistory
PastDiseases []contract.PastDisease
PastSurgeries []contract.PastSurgery
PastTraumas []contract.PastTrauma
PastTransfusions []contract.PastBloodTx
FamilyMiddle *contract.FamilyMiddle
}
type DictSnapshot struct {
@@ -60,6 +113,8 @@ type MasterDataLookup interface {
RegionCodeByName(name string) (string, bool)
DoctorIDByName(name string) (string, bool)
ManaUnitIDByName(name string) (string, bool)
DoctorIDExists(id string) bool
ManaUnitIDExists(id string) bool
}
type MapContext struct {
@@ -105,14 +160,25 @@ func MapHealthRecord(src PHISHealthRecord, ctx MapContext) (MappedHealthRecord,
}
mapped := MappedHealthRecord{
CheckID: GenerateCheckID(src.SourceSystem, "JKDA", src.SourceRecordID, src.UpdatedAt),
IDCard: src.IDCard,
PersonName: src.PersonName,
Birthday: src.Birthday,
MobileNumber: src.MobileNumber,
RegionCode: src.RegionCode,
ManaDoctorID: src.ManaDoctorID,
ManaUnitID: src.ManaUnitID,
CheckID: GenerateCheckID(src.SourceSystem, "JKDA", src.SourceRecordID),
IDCard: src.IDCard,
PersonName: src.PersonName,
Birthday: src.Birthday,
MobileNumber: src.MobileNumber,
RegionCode: src.RegionCode,
ManaDoctorID: src.ManaDoctorID,
ManaUnitID: src.ManaUnitID,
WorkPlace: src.WorkPlace,
Contact: src.Contact,
ContactPhone: src.ContactPhone,
RegisteredPermanent: src.RegisteredPermanent,
Address: src.Address,
AddressNumber: src.AddressNumber,
HomePlace: src.HomePlace,
HomePlaceNumber: src.HomePlaceNumber,
CardType: src.CardType,
IsFillSHHJ: strings.ToLower(strings.TrimSpace(src.IsFillSHHJ)),
CreateDate: src.CreateDate,
}
mapped.SexCode = mapCode(ctx.Dicts.Sex, src.SexCode, &errs)
mapped.NationCode = mapCode(ctx.Dicts.Nation, src.NationCode, &errs)
@@ -121,10 +187,79 @@ func MapHealthRecord(src PHISHealthRecord, ctx MapContext) (MappedHealthRecord,
mapped.MaritalStatusCode = mapCode(ctx.Dicts.MaritalStatus, src.MaritalStatusCode, &errs)
mapped.BloodTypeCode = mapCode(ctx.Dicts.BloodType, src.BloodTypeCode, &errs)
mapped.RhBloodCode = mapCode(ctx.Dicts.RhBlood, src.RhBloodCode, &errs)
mapped.InsuranceCode = mapCode(ctx.Dicts.Insurance, src.InsuranceCode, &errs)
mapped.InsuranceCode = mapDelimitedCode(ctx.Dicts.Insurance, src.InsuranceCode, &errs)
return mapped, errs
}
func MapHealthRecordTask(task source.HealthRecordTask, ctx MapContext) (contract.HealthRecordCreate, []ValidationError) {
r := task.Record
src := PHISHealthRecord{
SourceSystem: "PHIS", SourceRecordID: task.SourceRecordID(), UpdatedAt: r.UpdatedAt,
IDCard: r.IDCard, PersonName: r.PersonName, SexCode: r.SexCode, Birthday: r.Birthday,
MobileNumber: r.MobileNumber, RegionCode: r.RegionCode, ManaDoctorID: r.ManaDoctorID,
ManaUnitID: r.ManaUnitID, NationCode: r.NationCode, EducationCode: r.EducationCode,
WorkCode: r.WorkCode, MaritalStatusCode: r.MaritalStatusCode, BloodTypeCode: r.BloodTypeCode,
RhBloodCode: r.RhBloodCode, InsuranceCode: r.InsuranceCode, WorkPlace: r.WorkPlace,
Contact: r.Contact, ContactPhone: r.ContactPhone, RegisteredPermanent: r.RegisteredPermanent,
Address: r.Address, AddressNumber: r.AddressNumber, HomePlace: r.HomePlace,
HomePlaceNumber: r.HomePlaceNumber, CardType: firstNonEmpty(r.CardType, task.CardType),
IsFillSHHJ: r.IsFillSHHJ, DrugAllergy: r.DrugAllergy, ExposureHistory: r.ExposureHistory,
FamilyFather: r.FamilyFather, FamilyMother: r.FamilyMother, FamilySiblings: r.FamilySiblings,
FamilyChildren: r.FamilyChildren, GeneticDisease: r.GeneticDisease, Disability: r.Disability,
PastDiseaseCode: r.PastDiseaseCode, PastSurgeryCode: r.PastSurgeryCode,
PastTraumaCode: r.PastTraumaCode, PastTransfusionCode: r.PastTransfusionCode,
CookAirTool: r.CookAirTool, FuelType: r.FuelType, WaterSourceCode: r.WaterSourceCode,
Washroom: r.Washroom, LivestockColumn: r.LivestockColumn,
CreateDate: r.CreateDate,
}
mapped, errs := MapHealthRecord(src, ctx)
errs = append(errs, validateTaskFields(task)...)
errs = append(errs, validateMasterData(src, ctx.MasterData)...)
pastHistory := &contract.PastHistory{}
pastHistory.YWGMS = validateDelimitedCodes("ywgms", src.DrugAllergy, allergyCodes, &errs)
pastHistory.BLS = validateDelimitedCodes("bls", src.ExposureHistory, exposureCodes, &errs)
pastHistory.JZSFQN = validateDelimitedCodes("jzsfqn", src.FamilyFather, fatherCodes, &errs)
pastHistory.JZSMQ = validateDelimitedCodes("jzsmq", src.FamilyMother, motherCodes, &errs)
pastHistory.JZSXDJM = validateDelimitedCodes("jzsxdjm", src.FamilySiblings, siblingCodes, &errs)
pastHistory.JZSZN = validateDelimitedCodes("jzszn", src.FamilyChildren, childrenCodes, &errs)
pastHistory.YCBS = validateDelimitedCodes("ycbs", src.GeneticDisease, geneticCodes, &errs)
pastHistory.CJQK = validateDelimitedCodes("cjqk", src.Disability, disabilityCodes, &errs)
if *pastHistory != (contract.PastHistory{}) {
mapped.PastHistory = pastHistory
}
mapped.PastDiseases = mapPastDiseases(validateDelimitedCodes("jwsjbcode", src.PastDiseaseCode, diseaseCodes, &errs))
mapped.PastSurgeries = mapPastSurgeries(validateDelimitedCodes("jwssscode", src.PastSurgeryCode, surgeryCodes, &errs))
mapped.PastTraumas = mapPastTraumas(validateDelimitedCodes("jwswscode", src.PastTraumaCode, traumaCodes, &errs))
mapped.PastTransfusions = mapPastTransfusions(validateDelimitedCodes("jwssxcode", src.PastTransfusionCode, transfusionCodes, &errs))
if mapped.IsFillSHHJ == "y" {
mapped.FamilyMiddle = &contract.FamilyMiddle{
CookAirTool: validateSingleCode("cookAirTool", src.CookAirTool, cookAirCodes, &errs),
FuelType: validateSingleCode("fuelType", src.FuelType, fuelCodes, &errs),
WaterSourceCode: validateSingleCode("waterSourceCode", src.WaterSourceCode, waterCodes, &errs),
Washroom: validateSingleCode("washroom", src.Washroom, washroomCodes, &errs),
LivestockColumn: validateSingleCode("livestockColumn", src.LivestockColumn, livestockCodes, &errs),
IsFillSHHJ: "y",
}
}
return BuildHealthRecordCreate(mapped), errs
}
func validateMasterData(src PHISHealthRecord, lookup MasterDataLookup) []ValidationError {
if lookup == nil {
return []ValidationError{{Field: "masterData", Reason: "CHIS dictionary snapshot is required"}}
}
var errs []ValidationError
if src.ManaDoctorID != "" && !lookup.DoctorIDExists(src.ManaDoctorID) {
errs = append(errs, ValidationError{Field: "manaDoctorId", Code: src.ManaDoctorID, Reason: "not found in CHIS doctor dictionary"})
}
if src.ManaUnitID != "" && !lookup.ManaUnitIDExists(src.ManaUnitID) {
errs = append(errs, ValidationError{Field: "manaUnitId", Code: src.ManaUnitID, Reason: "not found in CHIS organization dictionary"})
}
return errs
}
func resolveMasterData(src *PHISHealthRecord, lookup MasterDataLookup) {
if lookup == nil {
return
@@ -160,31 +295,223 @@ func mapCode(dict Dict, code string, errs *[]ValidationError) string {
}
return mapped
}
func mapDelimitedCode(dict Dict, value string, errs *[]ValidationError) string {
parts := splitCodes(value)
mapped := make([]string, 0, len(parts))
for _, code := range parts {
mappedCode := mapCode(dict, code, errs)
if mappedCode != "" {
mapped = append(mapped, mappedCode)
}
}
return strings.Join(mapped, ",")
}
func BuildHealthRecordCreate(mapped MappedHealthRecord) contract.HealthRecordCreate {
return contract.HealthRecordCreate{
BaseInfo: contract.HealthRecordBaseInfo{
IDCard: mapped.IDCard,
PersonName: mapped.PersonName,
},
ManageInfo: contract.ManageInfo{OperateUser: mapped.ManaDoctorID},
HealthRecord: contract.HealthRecordCreateInfo{
CheckID: mapped.CheckID,
IDCard: mapped.IDCard,
PersonName: mapped.PersonName,
SexCode: mapped.SexCode,
Birthday: mapped.Birthday,
MobileNumber: mapped.MobileNumber,
RegionCode: mapped.RegionCode,
ManaDoctorID: mapped.ManaDoctorID,
ManaUnitID: mapped.ManaUnitID,
CreateUser: mapped.ManaDoctorID,
CreateUnit: mapped.ManaUnitID,
NationCode: mapped.NationCode,
EducationCode: mapped.EducationCode,
WorkCode: mapped.WorkCode,
MaritalStatusCode: mapped.MaritalStatusCode,
BloodTypeCode: mapped.BloodTypeCode,
RhBloodCode: mapped.RhBloodCode,
InsuranceCode: mapped.InsuranceCode,
CheckID: mapped.CheckID,
IDCard: mapped.IDCard,
PersonName: mapped.PersonName,
SexCode: mapped.SexCode,
Birthday: mapped.Birthday,
MobileNumber: mapped.MobileNumber,
WorkPlace: mapped.WorkPlace,
Contact: mapped.Contact,
ContactPhone: mapped.ContactPhone,
RegisteredPermanent: mapped.RegisteredPermanent,
RegionCode: mapped.RegionCode,
Address: mapped.Address,
AddressNumber: mapped.AddressNumber,
HomePlace: mapped.HomePlace,
HomePlaceNumber: mapped.HomePlaceNumber,
CardType: mapped.CardType,
ManaDoctorID: mapped.ManaDoctorID,
ManaUnitID: mapped.ManaUnitID,
CreateUser: mapped.ManaDoctorID,
CreateUnit: mapped.ManaUnitID,
NationCode: mapped.NationCode,
EducationCode: mapped.EducationCode,
WorkCode: mapped.WorkCode,
MaritalStatusCode: mapped.MaritalStatusCode,
BloodTypeCode: mapped.BloodTypeCode,
RhBloodCode: mapped.RhBloodCode,
InsuranceCode: mapped.InsuranceCode,
IsFillSHHJ: mapped.IsFillSHHJ,
CreateDate: mapped.CreateDate,
},
PastHistory: mapped.PastHistory,
PastDiseases: mapped.PastDiseases,
PastSurgeries: mapped.PastSurgeries,
PastTraumas: mapped.PastTraumas,
PastTransfusions: mapped.PastTransfusions,
FamilyMiddle: mapped.FamilyMiddle,
}
}
func validateTaskFields(task source.HealthRecordTask) []ValidationError {
r := task.Record
var errs []ValidationError
for _, field := range []struct{ name, value string }{
{"archId", task.ArchID}, {"contact", r.Contact}, {"contactPhone", r.ContactPhone},
{"registeredPermanent", r.RegisteredPermanent}, {"cardType", firstNonEmpty(r.CardType, task.CardType)},
{"nationCode", r.NationCode}, {"bloodTypeCode", r.BloodTypeCode}, {"rhBloodCode", r.RhBloodCode},
{"educationCode", r.EducationCode}, {"workCode", r.WorkCode}, {"maritalStatusCode", r.MaritalStatusCode},
{"insuranceCode", r.InsuranceCode}, {"ywgms", r.DrugAllergy}, {"cjqk", r.Disability},
} {
if strings.TrimSpace(field.value) == "" {
errs = append(errs, ValidationError{Field: field.name, Reason: "required"})
}
}
for _, field := range []struct {
name, value string
max int
}{
{"idCard", r.IDCard, 18}, {"personName", r.PersonName, 50}, {"workPlace", r.WorkPlace, 70},
{"mobileNumber", r.MobileNumber, 20}, {"contact", r.Contact, 50}, {"contactPhone", r.ContactPhone, 20},
{"address", r.Address, 100}, {"addressNumber", r.AddressNumber, 70}, {"homePlace", r.HomePlace, 100},
{"homePlaceNumber", r.HomePlaceNumber, 70}, {"manaDoctorId", r.ManaDoctorID, 20}, {"manaUnitId", r.ManaUnitID, 20},
} {
if utf8.RuneCountInString(field.value) > field.max {
errs = append(errs, ValidationError{Field: field.name, Reason: "max length " + strconv.Itoa(field.max)})
}
}
for _, field := range []struct {
name, value string
length int
}{
{"idCard", r.IDCard, 18},
{"regionCode", r.RegionCode, 12},
} {
if utf8.RuneCountInString(field.value) != field.length {
errs = append(errs, ValidationError{Field: field.name, Reason: "length " + strconv.Itoa(field.length)})
}
}
if _, err := time.Parse("2006-01-02", r.Birthday); err != nil {
errs = append(errs, ValidationError{Field: "birthday", Reason: "invalid date, want yyyy-MM-dd"})
}
if r.RegisteredPermanent != "1" && r.RegisteredPermanent != "2" {
errs = append(errs, ValidationError{Field: "registeredPermanent", Code: r.RegisteredPermanent, Reason: "unknown code"})
}
fill := strings.ToLower(strings.TrimSpace(r.IsFillSHHJ))
if fill != "y" && fill != "n" {
errs = append(errs, ValidationError{Field: "isFillShhj", Code: r.IsFillSHHJ, Reason: "unknown code"})
}
if task.Doctor.DoctorID != "" && task.Doctor.DoctorID != r.ManaDoctorID {
errs = append(errs, ValidationError{Field: "manaDoctorId", Reason: "does not match data.doctor.doctorId"})
}
return errs
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func splitCodes(value string) []string {
replacer := strings.NewReplacer(",", ",", "、", ",", ";", ",", ";", ",")
var out []string
for _, part := range strings.Split(replacer.Replace(value), ",") {
if code := strings.TrimSpace(part); code != "" {
out = append(out, code)
}
}
return out
}
func validateDelimitedCodes(field, value string, allowed map[string]struct{}, errs *[]ValidationError) string {
parts := splitCodes(value)
valid := make([]string, 0, len(parts))
for _, code := range parts {
if _, ok := allowed[code]; !ok {
*errs = append(*errs, ValidationError{Field: field, Code: code, Reason: "unknown code"})
continue
}
valid = append(valid, code)
}
return strings.Join(valid, ",")
}
func validateSingleCode(field, value string, allowed map[string]struct{}, errs *[]ValidationError) string {
if parts := splitCodes(value); len(parts) != 1 {
*errs = append(*errs, ValidationError{Field: field, Reason: "must contain exactly one code"})
return ""
}
return validateDelimitedCodes(field, value, allowed, errs)
}
func codeSet(values ...string) map[string]struct{} {
set := make(map[string]struct{}, len(values))
for _, value := range values {
set[value] = struct{}{}
}
return set
}
var (
allergyCodes = codeSet("0101", "0102", "0103", "0104", "0109")
exposureCodes = codeSet("1201", "1202", "1203", "1204")
fatherCodes = numberedCodes("07")
motherCodes = numberedCodes("08")
siblingCodes = numberedCodes("09")
childrenCodes = numberedCodes("10")
geneticCodes = codeSet("0501", "0502")
disabilityCodes = codeSet("1101", "1102", "1103", "1104", "1105", "1106", "1107", "1108", "1109", "1199")
diseaseCodes = codeSet("0201", "0202", "0203", "0204", "0205", "0206", "0207", "0208", "0209", "0210", "0211", "0212", "0213", "0214", "0215", "0298", "0299")
surgeryCodes = codeSet("0301", "0302")
traumaCodes = codeSet("0601", "0602")
transfusionCodes = codeSet("0401", "0402")
cookAirCodes = codeSet("1", "2", "3", "4", "9")
fuelCodes = codeSet("1", "2", "3", "4", "5", "9")
waterCodes = codeSet("1", "2", "3", "4", "5", "9")
washroomCodes = codeSet("1", "2", "3", "4", "5", "6")
livestockCodes = codeSet("0", "1", "2", "3")
)
func numberedCodes(prefix string) map[string]struct{} {
values := []string{prefix + "01", prefix + "02", prefix + "03", prefix + "04", prefix + "05", prefix + "06", prefix + "07", prefix + "08", prefix + "09", prefix + "10", prefix + "11", prefix + "99"}
return codeSet(values...)
}
func mapPastDiseases(value string) []contract.PastDisease {
parts := splitCodes(value)
out := make([]contract.PastDisease, 0, len(parts))
for _, code := range parts {
out = append(out, contract.PastDisease{Code: code})
}
return out
}
func mapPastSurgeries(value string) []contract.PastSurgery {
parts := splitCodes(value)
out := make([]contract.PastSurgery, 0, len(parts))
for _, code := range parts {
out = append(out, contract.PastSurgery{Code: code})
}
return out
}
func mapPastTraumas(value string) []contract.PastTrauma {
parts := splitCodes(value)
out := make([]contract.PastTrauma, 0, len(parts))
for _, code := range parts {
out = append(out, contract.PastTrauma{Code: code})
}
return out
}
func mapPastTransfusions(value string) []contract.PastBloodTx {
parts := splitCodes(value)
out := make([]contract.PastBloodTx, 0, len(parts))
for _, code := range parts {
out = append(out, contract.PastBloodTx{Code: code})
}
return out
}
+7 -4
View File
@@ -1,6 +1,9 @@
package mapping
import "testing"
import (
"reflect"
"testing"
)
func TestDocxStyleHealthRecordMappingBaseline(t *testing.T) {
src := PHISHealthRecord{
@@ -29,7 +32,7 @@ func TestDocxStyleHealthRecordMappingBaseline(t *testing.T) {
t.Fatalf("errors = %#v", errs)
}
assertMappedBaseline(t, got, MappedHealthRecord{
CheckID: GenerateCheckID("PHIS", "JKDA", "docx-sample-001", "20260707100000"),
CheckID: GenerateCheckID("PHIS", "JKDA", "docx-sample-001"),
IDCard: "440000********0001",
PersonName: "文档样例",
SexCode: "2",
@@ -74,7 +77,7 @@ func TestJointDebugStyleHealthRecordMappingBaseline(t *testing.T) {
if len(errs) != 0 {
t.Fatalf("errors = %#v", errs)
}
if got.CheckID != GenerateCheckID("PHIS", "JKDA", "joint-debug-001", "20260707110000") {
if got.CheckID != GenerateCheckID("PHIS", "JKDA", "joint-debug-001") {
t.Fatalf("checkId = %q", got.CheckID)
}
if got.IDCard != "440000********1234" || got.PersonName != "联调样例" || got.NationCode != "01" || got.WorkCode != "Y" {
@@ -113,7 +116,7 @@ func TestHealthRecordMappingBaselineRejectsDirtySample(t *testing.T) {
func assertMappedBaseline(t *testing.T, got MappedHealthRecord, want MappedHealthRecord) {
t.Helper()
if got != want {
if !reflect.DeepEqual(got, want) {
t.Fatalf("mapped = %#v\nwant = %#v", got, want)
}
}
+153
View File
@@ -0,0 +1,153 @@
package mapping
import (
"os"
"testing"
"chis_osi/source"
)
func TestMapHealthRecordTaskBuildsCompleteCHISRequest(t *testing.T) {
task := loadPHISHealthRecordTask(t)
req, errs := MapHealthRecordTask(task, validPHISMapContext())
if len(errs) != 0 {
t.Fatalf("errors = %#v", errs)
}
hr := req.HealthRecord
if hr.IDCard != task.Record.IDCard || hr.PersonName != task.Record.PersonName || hr.WorkPlace != task.Record.WorkPlace {
t.Fatalf("direct fields = %#v", hr)
}
if hr.Contact != task.Record.Contact || hr.ContactPhone != task.Record.ContactPhone || hr.RegisteredPermanent != "1" || hr.CardType != "01" {
t.Fatalf("contact/identity fields = %#v", hr)
}
if hr.AddressNumber != task.Record.AddressNumber || hr.Address != task.Record.Address || hr.HomePlace != task.Record.HomePlace || hr.HomePlaceNumber != task.Record.HomePlaceNumber {
t.Fatalf("address fields = %#v", hr)
}
if hr.CreateDate != task.Record.CreateDate {
t.Fatalf("createDate = %q, want %q", hr.CreateDate, task.Record.CreateDate)
}
if req.ManageInfo.OperateUser != task.Record.ManaDoctorID {
t.Fatalf("operateUser = %q", req.ManageInfo.OperateUser)
}
if req.PastHistory == nil || req.PastHistory.YWGMS != "0101" || req.PastHistory.BLS != "1201" || req.PastHistory.CJQK != "1101" {
t.Fatalf("pastHistory = %#v", req.PastHistory)
}
if len(req.PastDiseases) != 1 || req.PastDiseases[0].Code != "0201" {
t.Fatalf("past diseases = %#v", req.PastDiseases)
}
if len(req.PastSurgeries) != 1 || req.PastSurgeries[0].Code != "0301" || len(req.PastTraumas) != 1 || req.PastTraumas[0].Code != "0601" || len(req.PastTransfusions) != 1 || req.PastTransfusions[0].Code != "0401" {
t.Fatalf("past event arrays = %#v %#v %#v", req.PastSurgeries, req.PastTraumas, req.PastTransfusions)
}
if req.FamilyMiddle == nil || req.FamilyMiddle.CookAirTool != "2" || req.FamilyMiddle.FuelType != "3" || req.FamilyMiddle.WaterSourceCode != "1" || req.FamilyMiddle.Washroom != "1" || req.FamilyMiddle.LivestockColumn != "0" {
t.Fatalf("familyMiddle = %#v", req.FamilyMiddle)
}
firstCheckID := hr.CheckID
task.Record.UpdatedAt = "2026-07-16 11:22:33"
second, errs := MapHealthRecordTask(task, validPHISMapContext())
if len(errs) != 0 {
t.Fatalf("second errors = %#v", errs)
}
if firstCheckID == "" || firstCheckID != second.HealthRecord.CheckID {
t.Fatalf("checkId changed across source updates: %q != %q", firstCheckID, second.HealthRecord.CheckID)
}
}
func TestMapHealthRecordTaskNormalizesEnvironmentFlag(t *testing.T) {
task := loadPHISHealthRecordTask(t)
task.Record.IsFillSHHJ = " Y "
req, errs := MapHealthRecordTask(task, validPHISMapContext())
if len(errs) != 0 {
t.Fatalf("errors = %#v", errs)
}
if req.HealthRecord.IsFillSHHJ != "y" || req.FamilyMiddle == nil {
t.Fatalf("environment flag = %q familyMiddle=%#v", req.HealthRecord.IsFillSHHJ, req.FamilyMiddle)
}
}
func TestMapHealthRecordTaskOmitsEmptyPastHistory(t *testing.T) {
task := loadPHISHealthRecordTask(t)
task.Record.DrugAllergy = ""
task.Record.ExposureHistory = ""
task.Record.FamilyFather = ""
task.Record.FamilyMother = ""
task.Record.FamilySiblings = ""
task.Record.FamilyChildren = ""
task.Record.GeneticDisease = ""
task.Record.Disability = ""
req, _ := MapHealthRecordTask(task, validPHISMapContext())
if req.PastHistory != nil {
t.Fatalf("pastHistory = %#v, want nil", req.PastHistory)
}
}
func TestMapHealthRecordTaskOmitsEnvironmentWhenNotFilled(t *testing.T) {
task := loadPHISHealthRecordTask(t)
task.Record.IsFillSHHJ = "n"
task.Record.CookAirTool = ""
task.Record.FuelType = ""
task.Record.WaterSourceCode = ""
task.Record.Washroom = ""
task.Record.LivestockColumn = ""
req, errs := MapHealthRecordTask(task, validPHISMapContext())
if len(errs) != 0 {
t.Fatalf("errors = %#v", errs)
}
if req.FamilyMiddle != nil || req.HealthRecord.IsFillSHHJ != "n" {
t.Fatalf("request environment = %#v healthRecord=%#v", req.FamilyMiddle, req.HealthRecord)
}
}
func TestMapHealthRecordTaskRejectsInvalidDateLengthAndHistoryCode(t *testing.T) {
task := loadPHISHealthRecordTask(t)
task.Record.IDCard = "short"
task.Record.RegionCode = "441625"
task.Record.Birthday = "19800102"
task.Record.PersonName = "这是一个超过五十个字符限制的测试姓名这是一个超过五十个字符限制的测试姓名这是一个超过五十个字符限制的测试姓名"
task.Record.DrugAllergy = "9999"
task.Record.CookAirTool = "1,2"
_, errs := MapHealthRecordTask(task, validPHISMapContext())
assertValidationField(t, errs, "idCard", "length 18")
assertValidationField(t, errs, "regionCode", "length 12")
assertValidationField(t, errs, "birthday", "invalid date, want yyyy-MM-dd")
assertValidationField(t, errs, "personName", "max length 50")
assertValidationField(t, errs, "ywgms", "unknown code")
assertValidationField(t, errs, "cookAirTool", "must contain exactly one code")
}
func TestMapHealthRecordTaskRequiresKnownDoctorAndUnit(t *testing.T) {
task := loadPHISHealthRecordTask(t)
ctx := validPHISMapContext()
ctx.MasterData = rejectingMasterDataLookup{}
_, errs := MapHealthRecordTask(task, ctx)
assertValidationField(t, errs, "manaDoctorId", "not found in CHIS doctor dictionary")
assertValidationField(t, errs, "manaUnitId", "not found in CHIS organization dictionary")
}
func validPHISMapContext() MapContext {
return MapContext{Dicts: DefaultDictSnapshot(), MasterData: fakeMasterDataLookup{}}
}
type rejectingMasterDataLookup struct{ fakeMasterDataLookup }
func (rejectingMasterDataLookup) DoctorIDExists(string) bool { return false }
func (rejectingMasterDataLookup) ManaUnitIDExists(string) bool { return false }
func loadPHISHealthRecordTask(t *testing.T) source.HealthRecordTask {
t.Helper()
raw, err := os.ReadFile("../source/testdata/health_record.json")
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
task, err := source.DecodeHealthRecordTask(raw)
if err != nil {
t.Fatalf("DecodeHealthRecordTask: %v", err)
}
return task
}
+13 -5
View File
@@ -108,10 +108,10 @@ func TestMapHealthRecordUsesMasterDataLookupWhenCodesMissing(t *testing.T) {
t.Fatalf("master data not resolved: %#v", got)
}
}
func TestGenerateCheckIDIsDeterministicAndVersionSensitive(t *testing.T) {
first := GenerateCheckID("PHIS", "JKDA", "record-001", "20260707010101")
second := GenerateCheckID("PHIS", "JKDA", "record-001", "20260707010101")
third := GenerateCheckID("PHIS", "JKDA", "record-001", "20260708010101")
func TestGenerateCheckIDIsDeterministicForStableSourceRecord(t *testing.T) {
first := GenerateCheckID("PHIS", "JKDA", "record-001")
second := GenerateCheckID("PHIS", "JKDA", "record-001")
third := GenerateCheckID("PHIS", "JKDA", "record-002")
if first != second {
t.Fatalf("checkId not deterministic: %q != %q", first, second)
}
@@ -119,7 +119,7 @@ func TestGenerateCheckIDIsDeterministicAndVersionSensitive(t *testing.T) {
t.Fatalf("checkId length = %d, want 20", len(first))
}
if first == third {
t.Fatalf("version change did not affect checkId: %q", first)
t.Fatalf("different source record reused checkId: %q", first)
}
}
@@ -136,6 +136,14 @@ func (fakeMasterDataLookup) DoctorIDByName(name string) (string, bool) {
func (fakeMasterDataLookup) ManaUnitIDByName(name string) (string, bool) {
return map[string]string{"测试机构": "123456789"}[name], name == "测试机构"
}
func (fakeMasterDataLookup) DoctorIDExists(id string) bool {
return id == "D001" || id == "doc-001" || id == "DOC001"
}
func (fakeMasterDataLookup) ManaUnitIDExists(id string) bool {
return id == "123456789"
}
func validPHISHealthRecord() PHISHealthRecord {
return PHISHealthRecord{
SourceSystem: "PHIS",