Files

546 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mapping
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
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
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 {
Sex Dict
Nation Dict
Education Dict
Work Dict
MaritalStatus Dict
BloodType Dict
RhBlood Dict
Insurance Dict
}
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 {
Dicts DictSnapshot
MasterData MasterDataLookup
}
func DefaultDictSnapshot() DictSnapshot {
return DictSnapshot{
Sex: SexDict,
Nation: NationDict,
Education: EducationDict,
Work: WorkDict,
MaritalStatus: MaritalStatusDict,
BloodType: BloodTypeDict,
RhBlood: RhBloodDict,
Insurance: InsuranceDict,
}
}
func MapHealthRecord(src PHISHealthRecord, ctx MapContext) (MappedHealthRecord, []ValidationError) {
if ctx.Dicts.Sex.Name == "" {
ctx.Dicts = DefaultDictSnapshot()
}
resolveMasterData(&src, ctx.MasterData)
var errs []ValidationError
for _, field := range []struct {
name string
value string
}{
{name: "idCard", value: src.IDCard},
{name: "personName", value: src.PersonName},
{name: "sexCode", value: src.SexCode},
{name: "birthday", value: src.Birthday},
{name: "mobileNumber", value: src.MobileNumber},
{name: "regionCode", value: src.RegionCode},
{name: "manaDoctorId", value: src.ManaDoctorID},
{name: "manaUnitId", value: src.ManaUnitID},
} {
if field.value == "" {
errs = append(errs, ValidationError{Field: field.name, Reason: "required"})
}
}
mapped := MappedHealthRecord{
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)
mapped.EducationCode = mapCode(ctx.Dicts.Education, src.EducationCode, &errs)
mapped.WorkCode = mapCode(ctx.Dicts.Work, src.WorkCode, &errs)
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 = 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
}
// ValidateHealthRecordTask performs all local mapping validation without
// claiming that the supplied doctor or organization exists in CHIS.
func ValidateHealthRecordTask(task source.HealthRecordTask) []ValidationError {
_, errs := MapHealthRecordTask(task, MapContext{
Dicts: DefaultDictSnapshot(),
MasterData: submittedHealthRecordMasterData{
doctorID: task.Record.ManaDoctorID,
unitID: task.Record.ManaUnitID,
},
})
return errs
}
type submittedHealthRecordMasterData struct {
doctorID string
unitID string
}
func (m submittedHealthRecordMasterData) RegionCodeByName(string) (string, bool) { return "", false }
func (m submittedHealthRecordMasterData) DoctorIDByName(string) (string, bool) { return "", false }
func (m submittedHealthRecordMasterData) ManaUnitIDByName(string) (string, bool) { return "", false }
func (m submittedHealthRecordMasterData) DoctorIDExists(id string) bool {
return strings.TrimSpace(id) != "" && strings.TrimSpace(id) == strings.TrimSpace(m.doctorID)
}
func (m submittedHealthRecordMasterData) ManaUnitIDExists(id string) bool {
return strings.TrimSpace(id) != "" && strings.TrimSpace(id) == strings.TrimSpace(m.unitID)
}
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
}
if src.RegionCode == "" && src.RegionName != "" {
if code, ok := lookup.RegionCodeByName(src.RegionName); ok {
src.RegionCode = code
}
}
if src.ManaDoctorID == "" && src.ManaDoctorName != "" {
if code, ok := lookup.DoctorIDByName(src.ManaDoctorName); ok {
src.ManaDoctorID = code
}
}
if src.ManaUnitID == "" && src.ManaUnitName != "" {
if code, ok := lookup.ManaUnitIDByName(src.ManaUnitName); ok {
src.ManaUnitID = code
}
}
}
func mapCode(dict Dict, code string, errs *[]ValidationError) string {
if code == "" {
return ""
}
mapped, err := dict.PHISToOSI(code)
if err != nil {
if validationErr, ok := err.(ValidationError); ok {
*errs = append(*errs, validationErr)
return ""
}
*errs = append(*errs, ValidationError{Field: dict.Field, Code: code, Reason: err.Error()})
return ""
}
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,
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
}