feat(mapping): 完成 PHIS 健康档案转换(T-212)
This commit is contained in:
+2
-7
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user