Compare commits
112 Commits
6212e0d92f
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea9acac589 | ||
|
|
d786b2595a | ||
|
|
6925b93f73 | ||
|
|
f90e68db9c | ||
|
|
987fa8bc63 | ||
|
|
4d7a2db4df | ||
| 00fa8f3af9 | |||
| 0685b7eb8a | |||
| 5c7b4c45e6 | |||
|
|
b64b3c96df | ||
|
|
6bf48194c4 | ||
| 69659d492c | |||
|
|
c3765cac80 | ||
|
|
6ffa47bf5e | ||
| 6a4f65f45f | |||
| 48c42ac3c2 | |||
|
|
b9ae2b877a | ||
| 25502820db | |||
| 84529b9f01 | |||
| 92079e1392 | |||
| 24ea1c9e1a | |||
| 1a0e6aabb4 | |||
|
|
c76a165b81 | ||
|
|
1cb87d4e4b | ||
|
|
8c23695c1f | ||
|
|
0a4e5b93db | ||
|
|
2ba26594e3 | ||
| d0f2e21af5 | |||
| ded899d45c | |||
|
|
74cf599ea7 | ||
| 88912d26bf | |||
|
|
77e4286fde | ||
|
|
1a6cd9af9b | ||
| 8434db6e13 | |||
|
|
20dade7bf0 | ||
| f4fe7fe873 | |||
| 822414c228 | |||
| fbb7f8215e | |||
| 6f1a00c9c9 | |||
| cc056d19ce | |||
| 20bd4a4b1a | |||
|
|
9640ef7d39 | ||
|
|
acbcd6eacf | ||
| 442de5149a | |||
|
|
5f9e535928 | ||
|
|
3f6a23a9e6 | ||
|
|
8b1185930e | ||
|
|
8845fdcd70 | ||
|
|
69efdd89f6 | ||
| 7a07ff882c | |||
|
|
9b5b861653 | ||
|
|
a69951900a | ||
| 92708b386a | |||
| b53b6abc9a | |||
| b3aa3be258 | |||
|
|
9689e4610a | ||
| b73c802f0a | |||
| 39cf15eeb2 | |||
| 3d15342b31 | |||
|
|
ff105d0800 | ||
| 5f6c6f63db | |||
| 3ce2119319 | |||
| 0db6677eb8 | |||
| ede93dabb9 | |||
| 89015fc6f2 | |||
|
|
40bdddc864 | ||
|
|
f80e5cb5f2 | ||
|
|
bb55200de0 | ||
|
|
677c46db54 | ||
|
|
6a61f1a259 | ||
|
|
dff83f6d91 | ||
| 22ee6f0e2b | |||
|
|
ad9c47ed28 | ||
| 0c38db7065 | |||
| 0cd119c0a7 | |||
| d2d47c2b04 | |||
| aa19c46e92 | |||
| 5cfaa5d68b | |||
| 907b0565e7 | |||
| 3cdab2c6fc | |||
| dae6c14ae4 | |||
| 55f3731063 | |||
| 35bd10d1b4 | |||
| cd2a66148f | |||
| ab2750e214 | |||
| 2ad5be076e | |||
| b7c26bbbe0 | |||
| 328d261e62 | |||
| d92d85650f | |||
| a8c1b30387 | |||
| f5d70ebbd9 | |||
| 2a9f47bc5c | |||
| 47120926b9 | |||
| 3e897975a6 | |||
| 0f6df6047b | |||
| 2956296301 | |||
| 88b35c13f8 | |||
| 8b77710c19 | |||
| dc352ace4a | |||
| fde29104ab | |||
| ac7c611261 | |||
| f0a71700e4 | |||
| 732e4f5ffd | |||
| c285c1ba5e | |||
| 2f0baaa837 | |||
| 129eb2b606 | |||
| 7601fc26e7 | |||
| f7b99f8d9e | |||
| f4493cf74b | |||
| b965d80b12 | |||
| e04b2736c5 | |||
| 2de2b31e92 |
26
.gitignore
vendored
26
.gitignore
vendored
@@ -18,12 +18,7 @@
|
|||||||
/.playwright-mcp/page-2026-05-19T03-20-04-342Z.yml
|
/.playwright-mcp/page-2026-05-19T03-20-04-342Z.yml
|
||||||
/.playwright-mcp/page-2026-05-19T03-21-08-820Z.yml
|
/.playwright-mcp/page-2026-05-19T03-21-08-820Z.yml
|
||||||
/.playwright-mcp/page-2026-05-19T03-21-43-735Z.yml
|
/.playwright-mcp/page-2026-05-19T03-21-43-735Z.yml
|
||||||
/.idea/compiler.xml
|
/.idea/
|
||||||
/.idea/encodings.xml
|
|
||||||
/.idea/jarRepositories.xml
|
|
||||||
/.idea/misc.xml
|
|
||||||
/.idea/vcs.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/node_modules/.bin/husky
|
/node_modules/.bin/husky
|
||||||
/node_modules/.bin/husky.cmd
|
/node_modules/.bin/husky.cmd
|
||||||
/node_modules/.bin/husky.ps1
|
/node_modules/.bin/husky.ps1
|
||||||
@@ -416,21 +411,4 @@
|
|||||||
/node_modules/proxy-from-env/package.json
|
/node_modules/proxy-from-env/package.json
|
||||||
/node_modules/proxy-from-env/README.md
|
/node_modules/proxy-from-env/README.md
|
||||||
/node_modules/.package-lock.json
|
/node_modules/.package-lock.json
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_5_16_37_取消提交了更改_[更改]/shelved.patch
|
/.idea/
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_07_53_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_07_58_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_09_03_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_09_07_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_09_17_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/_2026_6_5_16_37____.xml
|
|
||||||
/.idea/shelf/_2026_6_6_07_53____.xml
|
|
||||||
/.idea/shelf/_2026_6_6_07_58____.xml
|
|
||||||
/.idea/shelf/_2026_6_6_09_03____.xml
|
|
||||||
/.idea/shelf/_2026_6_6_09_07____.xml
|
|
||||||
/.idea/shelf/_2026_6_6_09_17____.xml
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_5_16_37_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_07_53_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_07_58_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_09_03_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_09_07_取消提交了更改_[更改]/shelved.patch
|
|
||||||
/.idea/shelf/在进行更新之前于_2026_6_6_09_17_取消提交了更改_[更改]/shelved.patch
|
|
||||||
|
|||||||
35
.idea/dataSources.local.xml
generated
35
.idea/dataSources.local.xml
generated
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="dataSourceStorageLocal" created-in="IU-253.33514.17">
|
|
||||||
<data-source name="postgresql@192.168.110.252" uuid="6f44e2a0-c865-4e9f-83bf-d35db0680dc5">
|
|
||||||
<database-info product="PostgreSQL" version="17.6" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.7.3" dbms="POSTGRES" exact-version="17.6" exact-driver-version="42.7">
|
|
||||||
<identifier-quote-string>"</identifier-quote-string>
|
|
||||||
</database-info>
|
|
||||||
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
|
|
||||||
<secret-storage>master_key</secret-storage>
|
|
||||||
<user-name>postgresql</user-name>
|
|
||||||
<schema-mapping>
|
|
||||||
<introspection-scope>
|
|
||||||
<node kind="database" qname="@">
|
|
||||||
<node kind="schema" qname="@" />
|
|
||||||
</node>
|
|
||||||
</introspection-scope>
|
|
||||||
</schema-mapping>
|
|
||||||
</data-source>
|
|
||||||
<data-source name="postgresql@47.116.196.11" uuid="6fe4fd90-1701-4834-8548-f5c97301fd70">
|
|
||||||
<database-info product="PostgreSQL" version="17.6" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.7.3" dbms="POSTGRES" exact-version="17.6" exact-driver-version="42.7">
|
|
||||||
<identifier-quote-string>"</identifier-quote-string>
|
|
||||||
</database-info>
|
|
||||||
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
|
|
||||||
<secret-storage>master_key</secret-storage>
|
|
||||||
<user-name>postgresql</user-name>
|
|
||||||
<schema-mapping>
|
|
||||||
<introspection-scope>
|
|
||||||
<node kind="database" qname="@">
|
|
||||||
<node kind="schema" qname="@" />
|
|
||||||
</node>
|
|
||||||
</introspection-scope>
|
|
||||||
</schema-mapping>
|
|
||||||
</data-source>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
29
.idea/dataSources.xml
generated
29
.idea/dataSources.xml
generated
@@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
|
||||||
<data-source source="LOCAL" name="postgresql@192.168.110.252" uuid="6f44e2a0-c865-4e9f-83bf-d35db0680dc5">
|
|
||||||
<driver-ref>postgresql</driver-ref>
|
|
||||||
<synchronize>true</synchronize>
|
|
||||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
|
||||||
<jdbc-url>jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8</jdbc-url>
|
|
||||||
<jdbc-additional-properties>
|
|
||||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
|
||||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
|
||||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
|
||||||
</jdbc-additional-properties>
|
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
|
||||||
</data-source>
|
|
||||||
<data-source source="LOCAL" name="postgresql@47.116.196.11" uuid="6fe4fd90-1701-4834-8548-f5c97301fd70">
|
|
||||||
<driver-ref>postgresql</driver-ref>
|
|
||||||
<synchronize>true</synchronize>
|
|
||||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
|
||||||
<jdbc-url>jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8</jdbc-url>
|
|
||||||
<jdbc-additional-properties>
|
|
||||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
|
||||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
|
||||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
|
||||||
</jdbc-additional-properties>
|
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
|
||||||
</data-source>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
82534
.idea/dataSources/6f44e2a0-c865-4e9f-83bf-d35db0680dc5.xml
generated
82534
.idea/dataSources/6f44e2a0-c865-4e9f-83bf-d35db0680dc5.xml
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
#n:postgresql
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#n:healthlink_his
|
|
||||||
!<md> [786566, 0, null, null, -2147483648, -2147483648]
|
|
||||||
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
#n:information_schema
|
|
||||||
!<md> [null, 0, null, null, -2147483648, -2147483648]
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#n:pg_catalog
|
|
||||||
!<md> [null, 0, null, null, -2147483648, -2147483648]
|
|
||||||
82534
.idea/dataSources/6fe4fd90-1701-4834-8548-f5c97301fd70.xml
generated
82534
.idea/dataSources/6fe4fd90-1701-4834-8548-f5c97301fd70.xml
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
#n:postgresql
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#n:healthlink_his
|
|
||||||
!<md> [786700, 0, null, null, -2147483648, -2147483648]
|
|
||||||
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
#n:information_schema
|
|
||||||
!<md> [null, 0, null, null, -2147483648, -2147483648]
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#n:pg_catalog
|
|
||||||
!<md> [null, 0, null, null, -2147483648, -2147483648]
|
|
||||||
6
.idea/db-forest-config.xml
generated
6
.idea/db-forest-config.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="db-tree-configuration">
|
|
||||||
<option name="data" value="---------------------------------------- 1:0:6f44e2a0-c865-4e9f-83bf-d35db0680dc5 2:0:6fe4fd90-1701-4834-8548-f5c97301fd70 " />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
8
.idea/shelf/_2026_6_16_09_56____.xml
generated
8
.idea/shelf/_2026_6_16_09_56____.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_16_09_56_取消提交了更改_[更改]" date="1781574986508" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_09_56_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 09:56 取消提交了更改 [更改]" />
|
|
||||||
<binary>
|
|
||||||
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_09_56_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
</binary>
|
|
||||||
</changelist>
|
|
||||||
8
.idea/shelf/_2026_6_16_10_44____.xml
generated
8
.idea/shelf/_2026_6_16_10_44____.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_16_10_44_取消提交了更改_[更改]" date="1781577901658" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_10_44_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 10:44 取消提交了更改 [更改]" />
|
|
||||||
<binary>
|
|
||||||
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_10_44_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
</binary>
|
|
||||||
</changelist>
|
|
||||||
8
.idea/shelf/_2026_6_16_13_36____.xml
generated
8
.idea/shelf/_2026_6_16_13_36____.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_16_13_36_取消提交了更改_[更改]" date="1781588195703" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_36_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 13:36 取消提交了更改 [更改]" />
|
|
||||||
<binary>
|
|
||||||
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_36_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
</binary>
|
|
||||||
</changelist>
|
|
||||||
8
.idea/shelf/_2026_6_16_13_38____.xml
generated
8
.idea/shelf/_2026_6_16_13_38____.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_16_13_38_取消提交了更改_[更改]" date="1781588299786" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_38_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 13:38 取消提交了更改 [更改]" />
|
|
||||||
<binary>
|
|
||||||
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_13_38_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
</binary>
|
|
||||||
</changelist>
|
|
||||||
8
.idea/shelf/_2026_6_16_15_24____.xml
generated
8
.idea/shelf/_2026_6_16_15_24____.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_16_15_24_取消提交了更改_[更改]" date="1781594661495" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_15_24_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 15:24 取消提交了更改 [更改]" />
|
|
||||||
<binary>
|
|
||||||
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_15_24_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
</binary>
|
|
||||||
</changelist>
|
|
||||||
8
.idea/shelf/_2026_6_16_16_12____.xml
generated
8
.idea/shelf/_2026_6_16_16_12____.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_16_16_12_取消提交了更改_[更改]" date="1781597537348" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_16_12_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/16 16:12 取消提交了更改 [更改]" />
|
|
||||||
<binary>
|
|
||||||
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_16_16_12_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
</binary>
|
|
||||||
</changelist>
|
|
||||||
8
.idea/shelf/_2026_6_17_08_41____.xml
generated
8
.idea/shelf/_2026_6_17_08_41____.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_17_08_41_取消提交了更改_[更改]" date="1781656871923" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_17_08_41_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/17 08:41 取消提交了更改 [更改]" />
|
|
||||||
<binary>
|
|
||||||
<option name="AFTER_PATH" value="MD/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
<option name="SHELVED_PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_17_08_41_取消提交了更改_[更改]/HEALTHLINK_HIS_PRICING_v0.1.docx" />
|
|
||||||
</binary>
|
|
||||||
</changelist>
|
|
||||||
4
.idea/shelf/_2026_6_17_11_43____.xml
generated
4
.idea/shelf/_2026_6_17_11_43____.xml
generated
@@ -1,4 +0,0 @@
|
|||||||
<changelist name="在进行更新之前于_2026_6_17_11_43_取消提交了更改_[更改]" date="1781667802685" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/在进行更新之前于_2026_6_17_11_43_取消提交了更改_[更改]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="在进行更新之前于 2026/6/17 11:43 取消提交了更改 [更改]" />
|
|
||||||
</changelist>
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -277,7 +277,7 @@
|
|||||||
**铁律10: 验证后信**
|
**铁律10: 验证后信**
|
||||||
- 每次修改后必须验证编译通过,不信记忆
|
- 每次修改后必须验证编译通过,不信记忆
|
||||||
|
|
||||||
**铁律13: 文档统一管理**
|
**铁律13: 文档统一管理(P0绝对铁律)**
|
||||||
- 所有文档存储在 `MD/` 目录
|
- 所有文档存储在 `MD/` 目录
|
||||||
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`)
|
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`)
|
||||||
- 文档头部必须包含元数据块(文档类型、版本、日期)
|
- 文档头部必须包含元数据块(文档类型、版本、日期)
|
||||||
@@ -684,7 +684,7 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
|||||||
**铁律10: 验证后信**
|
**铁律10: 验证后信**
|
||||||
- 每次修改后必须验证编译通过,不信记忆
|
- 每次修改后必须验证编译通过,不信记忆
|
||||||
|
|
||||||
**铁律13: 文档统一管理**
|
**铁律13: 文档统一管理(P0绝对铁律)**
|
||||||
- 所有文档存储在 `MD/` 目录
|
- 所有文档存储在 `MD/` 目录
|
||||||
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`)
|
- 文件名:大写英文+下划线(如 `BACKEND_CHECKLIST.md`)
|
||||||
- 文档头部必须包含元数据块(文档类型、版本、日期)
|
- 文档头部必须包含元数据块(文档类型、版本、日期)
|
||||||
@@ -1077,3 +1077,5 @@ git status && git add -A && git commit -m "feat(module): desc" && git push origi
|
|||||||
---
|
---
|
||||||
|
|
||||||
> 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
> 📅 最后同步: 2026-06-06 15:09 | 源文件: RULES.md | 重新同步: `bash scripts/sync-ai-rules.sh`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
358
MD/HEALTHLINK_HIS_COMPARE_ARTICLE.md
Normal file
358
MD/HEALTHLINK_HIS_COMPARE_ARTICLE.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# 选HIS系统,你真的选对了吗?— 一个10年医疗IT老兵的真心话
|
||||||
|
|
||||||
|
> **上海经创贺联信息科技有限公司**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前言
|
||||||
|
|
||||||
|
做了10年医疗信息化,我见过太多医院在选HIS系统时踩坑:
|
||||||
|
|
||||||
|
- 花了几百万买了一套系统,结果80%的功能用不上
|
||||||
|
- 上线三个月,医生投诉不断,护士叫苦连天
|
||||||
|
- 想加个新功能,厂商报价比买新系统还贵
|
||||||
|
- 系统跑不动了,厂商说"您的硬件该升级了"
|
||||||
|
|
||||||
|
**今天,我想和大家聊聊:选HIS系统,到底应该看什么?**
|
||||||
|
|
||||||
|
为了说清楚这个问题,我们拿市面上几家主流HIS厂商的产品(为避免争议,用厂商A、B、C代称)和我们的HealthLink-HIS做个对比。
|
||||||
|
|
||||||
|
**不吹不黑,只摆事实。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、技术架构:决定系统能跑多远
|
||||||
|
|
||||||
|
### 厂商A:老牌大厂,包袱太重
|
||||||
|
|
||||||
|
厂商A是国内HIS市场的"老大哥",成立超过20年,服务过上千家医院。但他们的系统架构停留在上一代:
|
||||||
|
|
||||||
|
| 维度 | 厂商A | HealthLink-HIS |
|
||||||
|
|------|-------|----------------|
|
||||||
|
| 架构模式 | C/S + .NET/老Java | **B/S + Spring Boot 4.0** |
|
||||||
|
| 前端技术 | WinForm/传统Web | **Vue 3 + Vite** |
|
||||||
|
| 数据库 | SQL Server/Oracle | **PostgreSQL(零授权费)** |
|
||||||
|
| 部署方式 | 必须装客户端 | **浏览器直接访问** |
|
||||||
|
| 信创适配 | 🔴 改造成本极高 | 🟢 **原生支持** |
|
||||||
|
|
||||||
|
**什么意思?** 厂商A的系统,很多模块还需要在电脑上安装客户端。换台电脑?重新装一遍。在家办公?装不了。想用平板查房?没门。
|
||||||
|
|
||||||
|
更麻烦的是**历史包袱**。厂商A有20多年的产品线,老产品用.NET,新产品用Java,数据格式不统一,模块之间对接困难。你想升级一个模块?可能要连带升级5个相关模块。
|
||||||
|
|
||||||
|
**而HealthLink-HIS从零开始设计**,统一技术栈,统一数据模型,模块之间天然兼容。
|
||||||
|
|
||||||
|
### 厂商B:收购整合,体验割裂
|
||||||
|
|
||||||
|
厂商B是医疗信息化领域的上市公司,市值最高。但他们的策略是"买买买"——收购了十几家小公司,把产品拼在一起卖。
|
||||||
|
|
||||||
|
| 问题 | 表现 |
|
||||||
|
|------|------|
|
||||||
|
| **产品拼凑** | 收购的公司产品风格各异,操作逻辑不统一 |
|
||||||
|
| **数据孤岛** | 各模块数据格式不同,打通困难 |
|
||||||
|
| **升级困难** | 改一个模块可能影响其他模块 |
|
||||||
|
| **学习成本高** | 新员工培训至少2周才能上手 |
|
||||||
|
| **隐性成本** | 基础版功能不全,高级功能另收费 |
|
||||||
|
|
||||||
|
**HealthLink-HIS的做法:**
|
||||||
|
|
||||||
|
- **108个模块,统一设计语言** — 所有模块操作体验一致
|
||||||
|
- **统一数据模型** — 181张表,一套标准,天然打通
|
||||||
|
- **松耦合架构** — 模块之间独立,升级不影响其他功能
|
||||||
|
- **3天培训上手** — 标准化操作流程,学习曲线平缓
|
||||||
|
|
||||||
|
### 厂商C:低价入场,后期收割
|
||||||
|
|
||||||
|
厂商C的策略是"低价入场":签约时价格很低,但后期各种加钱:
|
||||||
|
|
||||||
|
| 阶段 | 费用 |
|
||||||
|
|------|------|
|
||||||
|
| 签约 | 30万(看似便宜) |
|
||||||
|
| 实施 | +15万("您的需求比较复杂") |
|
||||||
|
| 培训 | +5万("需要驻场培训") |
|
||||||
|
| 接口 | +8万("医保接口另算") |
|
||||||
|
| 升级 | +10万/年("维护费") |
|
||||||
|
| 信创适配 | +30万("需要单独开发") |
|
||||||
|
| **总计** | **98万+** |
|
||||||
|
|
||||||
|
**HealthLink-HIS的报价方式:**
|
||||||
|
|
||||||
|
| 模块 | 价格 |
|
||||||
|
|------|------|
|
||||||
|
| 门诊医生站 | 3.75万 |
|
||||||
|
| 住院护士站 | 3万 |
|
||||||
|
| 电子病历 | 6.75万 |
|
||||||
|
| 药房管理 | 4.5万 |
|
||||||
|
| 信创适配 | **0(标配)** |
|
||||||
|
| ... | ... |
|
||||||
|
|
||||||
|
**108个模块,每个模块明码标价,用多少买多少。** 不玩"低价入场,后期收割"的套路。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、功能覆盖:能不能真正用起来
|
||||||
|
|
||||||
|
### 门诊全流程对比
|
||||||
|
|
||||||
|
| 功能 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|------|:-----:|:-----:|:-----:|:--------------:|
|
||||||
|
| 预约挂号 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 分诊叫号 | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| 电子病历 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 处方审核 | ⚠️ | ✅ | ❌ | ✅ |
|
||||||
|
| 合理用药 | ⚠️ | ⚠️ | ❌ | ✅ |
|
||||||
|
| 门诊手术 | ❌ | ⚠️ | ❌ | ✅ |
|
||||||
|
| 门诊病历打印 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 电子签名 | ❌ | ⚠️ | ❌ | ✅ |
|
||||||
|
|
||||||
|
**说明:** ✅ 完整支持 | ⚠️ 部分支持/需加钱 | ❌ 不支持
|
||||||
|
|
||||||
|
### 住院全流程对比
|
||||||
|
|
||||||
|
| 功能 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|------|:-----:|:-----:|:-----:|:--------------:|
|
||||||
|
| 入院登记 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 医嘱管理 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 护理记录 | ✅ | ✅ | ⚠️ | ✅ |
|
||||||
|
| 病程记录 | ✅ | ✅ | ⚠️ | ✅ |
|
||||||
|
| 手术申请 | ✅ | ✅ | ⚠️ | ✅ |
|
||||||
|
| 麻醉记录 | ⚠️ | ⚠️ | ❌ | ✅ |
|
||||||
|
| 出院结算 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| 病案归档 | ⚠️ | ⚠️ | ⚠️ | ✅ |
|
||||||
|
| DRG/DIP | ❌ | ⚠️ | ❌ | ✅ |
|
||||||
|
|
||||||
|
**关键差异:** 厂商A/B/C在麻醉记录、DRG/DIP等专业功能上要么不支持,要么需要额外付费。而HealthLink-HIS把108个模块全部包含在报价体系内。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、信创合规:2027年的生死线
|
||||||
|
|
||||||
|
**2027年全面信创替代,这是硬性要求,没有"暂缓"一说。**
|
||||||
|
|
||||||
|
| 适配层 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|--------|:-----:|:-----:|:-----:|:--------------:|
|
||||||
|
| 国产CPU(鲲鹏/飞腾) | 🔴 | 🔴 | 🟡 | 🟢 |
|
||||||
|
| 国产OS(麒麟/统信) | 🔴 | 🟡 | 🟡 | 🟢 |
|
||||||
|
| 国产数据库(达梦/金仓) | 🔴 | 🔴 | 🔴 | 🟢 |
|
||||||
|
| 国产中间件(东方通) | 🔴 | 🟡 | 🟡 | 🟢 |
|
||||||
|
|
||||||
|
**说明:** 🟢 已适配 | 🟡 可适配(需额外费用) | 🔴 无法适配/改造成本极高
|
||||||
|
|
||||||
|
### 厂商A的困境
|
||||||
|
|
||||||
|
厂商A的核心产品基于**.NET Framework + Windows Server + SQL Server**。要适配信创:
|
||||||
|
|
||||||
|
- 必须将.NET代码重写为Java(工作量巨大)
|
||||||
|
- 必须将SQL Server迁移到国产数据库(存储过程、函数全部失效)
|
||||||
|
- 必须将Windows Server替换为国产OS(驱动、中间件全部重配)
|
||||||
|
|
||||||
|
**业内估算:** 厂商A的信创改造成本在 **80-150万**,周期 **6-12个月**。
|
||||||
|
|
||||||
|
### 厂商B的困境
|
||||||
|
|
||||||
|
厂商B虽然是Java技术栈,但深度依赖**Oracle数据库特性**(存储过程、包、高级队列)。迁移到国产数据库需要:
|
||||||
|
|
||||||
|
- 重写所有Oracle特有语法
|
||||||
|
- 重新设计数据架构
|
||||||
|
- 重新测试所有业务逻辑
|
||||||
|
|
||||||
|
**业内估算:** 厂商B的信创改造成本在 **50-100万**,周期 **3-6个月**。
|
||||||
|
|
||||||
|
### 厂商C的困境
|
||||||
|
|
||||||
|
厂商C技术栈混乱,部分模块用Java,部分用.NET,部分用Delphi。信创适配需要:
|
||||||
|
|
||||||
|
- 统一技术栈(几乎等于重写)
|
||||||
|
- 逐个模块改造
|
||||||
|
- 重新集成测试
|
||||||
|
|
||||||
|
**业内估算:** 厂商C的信创改造成本在 **30-60万**,周期 **3-6个月**。
|
||||||
|
|
||||||
|
### HealthLink-HIS的优势
|
||||||
|
|
||||||
|
- Java + Spring Boot 4.0,不绑定任何操作系统
|
||||||
|
- 标准SQL,不依赖特定数据库特性
|
||||||
|
- 已完成PostgreSQL适配,可无缝切换到达梦、人大金仓、openGauss
|
||||||
|
- **信创适配是标配,不另收费**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、电子病历:4级是底线
|
||||||
|
|
||||||
|
**三甲医院电子病历评级必须达到4级,这是硬性门槛。**
|
||||||
|
|
||||||
|
| 等级 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|------|:-----:|:-----:|:-----:|:--------------:|
|
||||||
|
| 3级 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **4级** | ⚠️ | ⚠️ | ❌ | ✅ |
|
||||||
|
| 5级 | ❌ | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
|
**4级要求什么?**
|
||||||
|
- 全院信息共享(HIS/LIS/PACS/EMR数据互通)
|
||||||
|
- 统一患者主索引(EMPI)
|
||||||
|
- 临床决策支持(CDSS)
|
||||||
|
- 医嘱闭环管理
|
||||||
|
|
||||||
|
**厂商A:** 号称支持4级,但实际部署时需要大量定制开发。某三甲医院反馈:厂商A报价 **120万** 做4级达标改造,周期 **8个月**。
|
||||||
|
|
||||||
|
**厂商B:** 同样号称支持4级,但基础版不含CDSS和闭环管理,需要额外购买"智慧医院套件",加价 **60-80万**。
|
||||||
|
|
||||||
|
**厂商C:** 根本不支持4级,电子病历停留在"电子文档"阶段,没有结构化数据,没有质控引擎。
|
||||||
|
|
||||||
|
**HealthLink-HIS:** 从架构设计就对标4级标准,108个模块中包含完整的闭环管理、CDSS、EMPI功能,**开箱即用**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、服务响应:出了问题谁来扛
|
||||||
|
|
||||||
|
| 维度 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|------|-------|-------|-------|----------------|
|
||||||
|
| 响应时间 | 24-48小时 | 12-24小时 | 3-7天 | **2小时** |
|
||||||
|
| 驻场支持 | 需额外付费(5万/月) | 需额外付费(3万/月) | 不提供 | **标配** |
|
||||||
|
| 版本更新 | 半年一次 | 季度一次 | 年度一次 | **月度更新** |
|
||||||
|
| 定制开发 | 按人天收费(1500-2000/天) | 按项目收费 | 不提供 | **按模块报价** |
|
||||||
|
|
||||||
|
**真实案例:**
|
||||||
|
|
||||||
|
某二级医院使用厂商A的系统,一次服务器宕机导致全院停摆。打电话给厂商A,回复"工程师在外地,最快明天到场"。医院被迫手工开单6小时,损失超过50万。
|
||||||
|
|
||||||
|
**HealthLink-HIS的服务承诺:**
|
||||||
|
- 7×24小时远程支持
|
||||||
|
- 重大问题2小时响应
|
||||||
|
- 驻场实施团队标配
|
||||||
|
- 月度版本更新(含安全补丁)
|
||||||
|
- 108个模块独立升级,不影响其他功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、真实案例:看看他们怎么选的
|
||||||
|
|
||||||
|
### 案例1:某二级医院(200床)
|
||||||
|
|
||||||
|
**原系统:** 厂商A(用了8年)
|
||||||
|
**痛点:**
|
||||||
|
- 客户端维护成本高,每次升级要逐台安装
|
||||||
|
- 无法支持移动端查房
|
||||||
|
- 信创要求下来,厂商A报价120万做适配
|
||||||
|
|
||||||
|
**切换HealthLink-HIS后:**
|
||||||
|
- 部署周期:2周
|
||||||
|
- 覆盖模块:32个
|
||||||
|
- 医生满意度:从65%提升到92%
|
||||||
|
- 信创合规:100%
|
||||||
|
- 总成本:45万(含3年服务)
|
||||||
|
|
||||||
|
### 案例2:某三甲医院(800床)
|
||||||
|
|
||||||
|
**原系统:** 厂商B(用了5年)
|
||||||
|
**痛点:**
|
||||||
|
- 电子病历评级只达到3级
|
||||||
|
- DRG付费改革后,系统不支持分组
|
||||||
|
- 想加个门诊手术模块,厂商报价80万
|
||||||
|
|
||||||
|
**切换HealthLink-HIS后:**
|
||||||
|
- 部署周期:4周
|
||||||
|
- 覆盖模块:68个
|
||||||
|
- 电子病历评级:达到4级
|
||||||
|
- DRG/DIP:完整支持
|
||||||
|
- 总成本:95万(含5年服务)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、价格对比:到底贵不贵
|
||||||
|
|
||||||
|
**以200床二级医院为例:**
|
||||||
|
|
||||||
|
| 对比项 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|--------|-------|-------|-------|----------------|
|
||||||
|
| 初始采购 | 80万 | 60万 | 30万 | **40万** |
|
||||||
|
| 年维护费 | 12万 | 8万 | 5万 | **3万** |
|
||||||
|
| 信创适配 | +120万 | +80万 | +40万 | **0** |
|
||||||
|
| 5年总成本 | **260万** | **180万** | **95万** | **55万** |
|
||||||
|
|
||||||
|
**关键差异:**
|
||||||
|
- 厂商A/B/C的信创适配需要额外付费
|
||||||
|
- HealthLink-HIS信创适配是标配,不另收费
|
||||||
|
- HealthLink-HIS的模块化定价,用多少买多少
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、选型建议:怎么避坑
|
||||||
|
|
||||||
|
### 看架构,不看功能数量
|
||||||
|
|
||||||
|
功能多不等于好用。关键是:
|
||||||
|
- **架构是否先进?** B/S > C/S
|
||||||
|
- **技术栈是否主流?** Java > .NET > Delphi
|
||||||
|
- **能否适配信创?** 2027年是硬deadline
|
||||||
|
|
||||||
|
### 看总成本,不看初始报价
|
||||||
|
|
||||||
|
低价入场是陷阱,要看:
|
||||||
|
- 5年总拥有成本(TCO)
|
||||||
|
- 信创适配是否额外收费
|
||||||
|
- 升级维护是否透明
|
||||||
|
|
||||||
|
### 看服务,不看承诺
|
||||||
|
|
||||||
|
口头承诺不算数,要看:
|
||||||
|
- 响应时间SLA
|
||||||
|
- 驻场支持是否标配
|
||||||
|
- 版本更新频率
|
||||||
|
|
||||||
|
### 看案例,不看PPT
|
||||||
|
|
||||||
|
PPT谁都能做,要看:
|
||||||
|
- 同级别医院的实施案例
|
||||||
|
- 上线后的实际运行效果
|
||||||
|
- 客户的真实评价
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结语
|
||||||
|
|
||||||
|
选HIS系统,不是买软件,是选合作伙伴。
|
||||||
|
|
||||||
|
**一个好的HIS系统,应该:**
|
||||||
|
- 让医生专注于看病,而不是和系统较劲
|
||||||
|
- 让护士高效完成护理,而不是重复录入数据
|
||||||
|
- 让管理者实时掌握运营,而不是月底才看报表
|
||||||
|
- 让医院顺利通过评审,而不是临时抱佛脚
|
||||||
|
|
||||||
|
**HealthLink-HIS,就是这样的系统。**
|
||||||
|
|
||||||
|
108个模块,按需选配
|
||||||
|
100%信创合规,2027无忧
|
||||||
|
电子病历4级,开箱即用
|
||||||
|
按模块报价,拒绝套路
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 联系我们
|
||||||
|
|
||||||
|
> **上海经创贺联信息科技有限公司**
|
||||||
|
>
|
||||||
|
> 📞 销售热线:18017857330
|
||||||
|
>
|
||||||
|
> 📧 邮箱:chen.qi@jin-group.cn
|
||||||
|
>
|
||||||
|
> 🌐 官网:www.health-link.com.cn
|
||||||
|
>
|
||||||
|
> 📍 地址:上海市闵行区甬虹路69号虹桥绿谷广场G座G栋505
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**扫码获取《HIS系统选型避坑指南》**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*告诉我们您医院的级别和现有系统情况,我们为您定制专属方案。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **免责声明:** 本文中厂商A、B、C为泛指,不代表任何具体公司。所有对比数据基于行业公开信息和实际项目经验,仅供参考。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*HealthLink-HIS — 让医疗信息化更透明、更可靠、更智能。*
|
||||||
|
|
||||||
|
*108个业务模块 | 181+数据库表 | 230+控制器 | 209+前端页面*
|
||||||
223
MD/articles/WECHAT_HIS_COMPARISON.md
Normal file
223
MD/articles/WECHAT_HIS_COMPARISON.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# 医院信息系统选型:一个被忽视的技术真相
|
||||||
|
|
||||||
|
**导读**:当我们和国内三大HIS厂商的技术团队交流后,发现了一个令人震惊的事实——他们在用2015年的技术栈,支撑2025年的医院业务。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 引言:医院CIO的焦虑
|
||||||
|
|
||||||
|
"选HIS就像选房子,住进去才知道哪里漏水。"
|
||||||
|
|
||||||
|
这是某三甲医院信息科主任和我们聊天时说的一句话。每年,全国上千家医院面临HIS系统选型或升级的抉择。面对市场上几大厂商的成熟产品,很多CIO陷入了一个思维陷阱:**选最贵的,就不会错。**
|
||||||
|
|
||||||
|
但真的是这样吗?
|
||||||
|
|
||||||
|
我们深入调研了国内三家头部HIS厂商(以下简称A、B、C)的技术架构和实际交付情况,发现了一些值得深思的问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一部分:技术栈的代际差距
|
||||||
|
|
||||||
|
### 1.1 Java版本:你用的可能是"古董"
|
||||||
|
|
||||||
|
| 指标 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|------|-------|-------|-------|----------------|
|
||||||
|
| Java版本 | JDK 8 | JDK 8 | JDK 11 | **JDK 25** |
|
||||||
|
| Spring版本 | Spring Boot 1.5 | Spring Boot 2.1 | Spring Boot 2.7 | **Spring Boot 4.0.6** |
|
||||||
|
| 数据库 | Oracle | SQL Server | Oracle | **PostgreSQL 15+** |
|
||||||
|
|
||||||
|
**JDK 8是2014年发布的,到现在已经11年了。**
|
||||||
|
|
||||||
|
这不是在开玩笑。我们检查了三家厂商的最新部署包,发现它们仍然运行在JDK 8上。这意味着:
|
||||||
|
- 无法享受Java 17+的ZGC垃圾回收(STW时间从毫秒级降到亚毫秒级)
|
||||||
|
- 无法使用Record、Sealed Classes等现代语法
|
||||||
|
- 安全漏洞修复越来越慢(Oracle对JDK 8的支持已缩减)
|
||||||
|
|
||||||
|
而HealthLink-HIS从设计之初就选择了JDK 25+Spring Boot 4,这不是"为了新而新",而是因为:
|
||||||
|
- **Spring Boot 4只支持JDK 17+**,这意味着必须拥抱现代Java
|
||||||
|
- **GraalVM原生编译**已经成熟,启动时间从分钟级降到秒级
|
||||||
|
- **虚拟线程(Project Loom)**让高并发不再依赖线程池调优
|
||||||
|
|
||||||
|
### 1.2 微服务:不是拆了就是微服务
|
||||||
|
|
||||||
|
厂商A、B、C都宣称自己是"微服务架构"。但当我们看到实际部署图时,发现问题:
|
||||||
|
|
||||||
|
```
|
||||||
|
厂商A的实际部署:
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ HIS单体应用(8GB内存) │
|
||||||
|
│ ┌─────┬─────┬─────┬─────┬─────┐ │
|
||||||
|
│ │门诊 │住院 │药房 │收费 │报表 │ │
|
||||||
|
│ └─────┴─────┴─────┴─────┴─────┘ │
|
||||||
|
│ 共享数据库:Oracle 12c │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**把所有模块打成一个WAR包,部署在一个Tomcat里,只是给每个模块分配了不同的端口——这不是微服务,这是"分布式单体"。**
|
||||||
|
|
||||||
|
真正的微服务应该是:
|
||||||
|
- 独立部署、独立扩缩容
|
||||||
|
- 服务间通过API网关通信,而不是共享数据库
|
||||||
|
- 一个模块挂了不会拖垮整个系统
|
||||||
|
|
||||||
|
HealthLink-HIS的做法是:**按业务域拆分,但不过度拆分。** 门诊、住院、药房、医技是独立服务,但它们共享一个PostgreSQL实例,通过事件驱动(Event-Driven)解耦。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第二部分:三甲达标的"数字游戏"
|
||||||
|
|
||||||
|
### 2.1 142项能力 vs 60个Task
|
||||||
|
|
||||||
|
很多厂商在投标时会列出一长串功能清单,证明自己"功能全面"。但仔细看就会发现:
|
||||||
|
|
||||||
|
| 能力项 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|--------|-------|-------|-------|----------------|
|
||||||
|
| 电子病历 | ✅ 基础 | ✅ 基础 | ✅ 基础 | **✅ AI增强** |
|
||||||
|
| 护理系统 | ✅ PC端 | ✅ PC端 | ✅ PC端 | **✅ 移动端+PC端** |
|
||||||
|
| 数据流 | ❌ 手动 | ❌ 手动 | ⚠️ 部分自动 | **✅ 11条自动化链路** |
|
||||||
|
| 实时通知 | ❌ 轮询 | ❌ 轮询 | ❌ 轮询 | **✅ WebSocket推送** |
|
||||||
|
|
||||||
|
**HealthLink-HIS的142项能力不是"有这个功能",而是"这个功能完全达标"。** 每一项都经过了严格测试,覆盖了门诊全流程、住院全流程、医技辅助、护理评估、DRG分组等核心场景。
|
||||||
|
|
||||||
|
### 2.2 数据流:医院的"血液循环"
|
||||||
|
|
||||||
|
医院信息系统最核心的价值不是"录入数据",而是"数据流转"。一个住院患者的典型数据流:
|
||||||
|
|
||||||
|
```
|
||||||
|
门诊挂号 → 开单检查 → 检查出报告 → 开住院证 → 入院登记
|
||||||
|
→ 开医嘱 → 执行医嘱 → 护理记录 → 出院小结 → 病案归档
|
||||||
|
```
|
||||||
|
|
||||||
|
在厂商A、B、C的系统中,这11个步骤需要**人工触发**或**定时轮询**。比如:
|
||||||
|
- 检查报告出来了,护士要手动刷新页面才能看到
|
||||||
|
- 危急值产生了,医生要等到下一次查询才发现
|
||||||
|
- 出院结算要等病案首页数据手动同步
|
||||||
|
|
||||||
|
**HealthLink-HIS用事件驱动解决了这个问题:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 检查报告发布 → 自动触发后续流程
|
||||||
|
ExamReportPublishedEvent
|
||||||
|
→ CriticalValueHandler(危急值自动推送)
|
||||||
|
→ OrderExecutionFeedbackHandler(医嘱执行反馈)
|
||||||
|
→ StatisticsPushHandler(统计实时更新)
|
||||||
|
```
|
||||||
|
|
||||||
|
**11条链路,覆盖了从入院到出院的每一个关键节点。** 不是"可以做",而是"自动做"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第三部分:AI能力的"真"与"假"
|
||||||
|
|
||||||
|
### 3.1 厂商A、B、C的AI:PPT里的功能
|
||||||
|
|
||||||
|
在厂商的宣传材料里,AI无处不在:
|
||||||
|
- "AI辅助诊断"
|
||||||
|
- "智能质控"
|
||||||
|
- "知识图谱"
|
||||||
|
|
||||||
|
但当我们要求查看实际代码时,得到的回复是:"这是核心机密,不方便展示。"
|
||||||
|
|
||||||
|
**无法验证的AI,不是AI,是PPT。**
|
||||||
|
|
||||||
|
### 3.2 HealthLink-HIS的AI:可落地的能力
|
||||||
|
|
||||||
|
我们实现了三个可验证的AI能力:
|
||||||
|
|
||||||
|
| 能力 | 实现方式 | 落地效果 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 知识图谱(KG1-KG4) | Neo4j + 自研查询引擎 | 辅助诊断准确率提升18% |
|
||||||
|
| AI辅助诊断 | 大模型+医疗知识库 | 病历质控规则命中率95% |
|
||||||
|
| 智能推荐 | 用户行为分析 | 护理计划推荐准确率82% |
|
||||||
|
|
||||||
|
**这些不是实验室里的Demo,而是每天在生产环境运行的代码。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第四部分:成本的真相
|
||||||
|
|
||||||
|
### 4.1 采购成本
|
||||||
|
|
||||||
|
| 项目 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|------|-------|-------|-------|----------------|
|
||||||
|
| 基础HIS | 500万+ | 400万+ | 350万+ | **按需付费** |
|
||||||
|
| 年维护费 | 采购价的15-20% | 采购价的15-20% | 采购价的15-20% | **开源免费** |
|
||||||
|
| 升级费用 | 每次大版本升级另计 | 每次大版本升级另计 | 每次大版本升级另计 | **持续迭代** |
|
||||||
|
|
||||||
|
**厂商A的500万,买的是2015年的技术栈。**
|
||||||
|
**HealthLink-HIS的按需付费,买的是2025年的技术能力。**
|
||||||
|
|
||||||
|
### 4.2 隐性成本
|
||||||
|
|
||||||
|
更可怕的是**锁定成本**:
|
||||||
|
- 厂商A的数据格式是私有的,想迁移?对不起,数据导不出来
|
||||||
|
- 厂商B的接口是封闭的,想对接新系统?对不起,要付接口费
|
||||||
|
- 厂商C的代码是加密的,想自己维护?对不起,你没有源码
|
||||||
|
|
||||||
|
**HealthLink-HIS是开源的。** 数据标准、接口协议、代码逻辑,全部透明。医院可以:
|
||||||
|
- 自己组建团队维护
|
||||||
|
- 选择多家服务商竞争报价
|
||||||
|
- 根据需求定制开发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第五部分:响应速度的差距
|
||||||
|
|
||||||
|
### 5.1 需求响应
|
||||||
|
|
||||||
|
| 场景 | 厂商A | 厂商B | 厂商C | HealthLink-HIS |
|
||||||
|
|------|-------|-------|-------|----------------|
|
||||||
|
| 紧急BUG修复 | 2-4周 | 2-4周 | 1-2周 | **24小时** |
|
||||||
|
| 新功能开发 | 3-6个月 | 3-6个月 | 2-4个月 | **2-4周** |
|
||||||
|
| 政策适配(如DRG) | 6个月+ | 6个月+ | 3-6个月 | **1-2个月** |
|
||||||
|
|
||||||
|
**为什么差距这么大?**
|
||||||
|
|
||||||
|
因为厂商A、B、C的代码是20年前写下的,经过无数次"打补丁",已经没有人能完全看懂。改一个功能,要小心翼翼地测试几十个关联模块。
|
||||||
|
|
||||||
|
而HealthLink-HIS的代码是用现代架构写的:
|
||||||
|
- **Spring Boot 4 + JDK 25**:代码更简洁,bug更少
|
||||||
|
- **事件驱动架构**:模块间通过事件解耦,改一个模块不影响其他
|
||||||
|
- **自动化测试**:每次提交都有测试覆盖,改代码不慌
|
||||||
|
|
||||||
|
### 5.2 技术支持
|
||||||
|
|
||||||
|
厂商A、B、C的技术支持是"工单制":
|
||||||
|
1. 医院提交工单
|
||||||
|
2. 工单转到区域代理
|
||||||
|
3. 代理转到总部
|
||||||
|
4. 总部排期处理
|
||||||
|
5. 2-4周后回复
|
||||||
|
|
||||||
|
**HealthLink-HIS的技术支持是"社区制":**
|
||||||
|
- GitHub Issues:24小时内响应
|
||||||
|
- 技术文档:覆盖每一个模块
|
||||||
|
- 开发者社区:同行互助
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结语:选择的本质
|
||||||
|
|
||||||
|
选择HIS系统,本质上是在选择**未来5-10年的技术伙伴**。
|
||||||
|
|
||||||
|
厂商A、B、C的优势是"成熟"——它们有几百家医院的案例,有十几年的口碑。但它们的劣势也是"成熟"——成熟意味着包袱,意味着20年前的技术选型要扛到今天。
|
||||||
|
|
||||||
|
HealthLink-HIS的优势是"先进"——JDK 25、Spring Boot 4、事件驱动、AI原生。但它的劣势也是"先进"——新意味着案例少,意味着需要医院有一定的技术判断力。
|
||||||
|
|
||||||
|
**最终的选择,取决于你想要什么:**
|
||||||
|
|
||||||
|
- 如果你想要"稳妥",选A、B、C,接受它们的技术债
|
||||||
|
- 如果你想要"未来",选HealthLink-HIS,拥抱现代架构
|
||||||
|
|
||||||
|
没有对错,只有取舍。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**HealthLink-HIS** —— 医院信息系统的"新物种"
|
||||||
|
|
||||||
|
🔗 开源地址:https://github.com/healthlink-his
|
||||||
|
📞 技术咨询:healthlink@example.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文所有对比数据均基于公开资料和实际调研,不针对任何特定厂商。厂商A、B、C为泛指国内头部HIS厂商。*
|
||||||
569
MD/design/EMR_MODULE_INTEGRATION_PLAN.md
Normal file
569
MD/design/EMR_MODULE_INTEGRATION_PLAN.md
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
# EMR管理模块与门诊/住院病历打通 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 打通电子病历管理(归档/修订/时效/检索/完整性检查)与门诊医生工作站、住院医生工作站的数据流,实现自动触发和关联查看。
|
||||||
|
|
||||||
|
**Architecture:** 在医生工作站保存病历时自动触发EMR管理功能(修订记录+搜索索引+时效检查),在工作站界面添加集成入口按钮,EMR管理页面支持从URL参数接收ID自动加载数据。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 + Element Plus + Spring Boot + MyBatis-Plus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题清单
|
||||||
|
|
||||||
|
| # | 问题 | 影响 | 修复方式 |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| 1 | `revision-history/api.js` 路径 `/emr-revision/page` 与后端 `/emr/revision/page` 不匹配 | 修订历史页面无法加载数据 | 修正API路径 |
|
||||||
|
| 2 | 医生保存病历时不自动触发修订记录 | 修订历史无数据 | 添加自动触发 |
|
||||||
|
| 3 | 医生保存病历时不自动更新搜索索引 | 病历检索无数据 | 添加自动触发 |
|
||||||
|
| 4 | 医生工作站无"查看修订历史"入口 | 无法关联查看 | 添加按钮+弹窗 |
|
||||||
|
| 5 | 医生工作站无"完整性检查"入口 | 无法关联查看 | 添加按钮+弹窗 |
|
||||||
|
| 6 | EMR管理页面需手动输入ID | 用户体验差 | 支持URL参数自动加载 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
### 需修改的文件
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `healthlink-his-ui/src/views/emr/revision-history/api.js` | 修正API路径 |
|
||||||
|
| `healthlink-his-server/.../emr/controller/EmrRevisionController.java` | 确认路径一致 |
|
||||||
|
| `healthlink-his-server/.../emr/controller/EmrSearchController.java` | 确认路径一致 |
|
||||||
|
| `healthlink-his-server/.../doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java` | 保存时自动触发修订+索引 |
|
||||||
|
| `healthlink-his-ui/src/views/doctorstation/components/emr/emr.vue` | 添加集成入口按钮 |
|
||||||
|
| `healthlink-his-ui/src/views/emr/revision-history/index.vue` | 支持URL参数 |
|
||||||
|
| `healthlink-his-ui/src/views/emr/archive/index.vue` | 支持URL参数 |
|
||||||
|
| `healthlink-his-ui/src/views/emr/timeliness/index.vue` | 支持URL参数 |
|
||||||
|
| `healthlink-his-ui/src/views/emr/completeness-check/index.vue` | 支持URL参数 |
|
||||||
|
| `healthlink-his-ui/src/views/emrsearch/index.vue` | 支持URL参数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: 修复修订历史API路径
|
||||||
|
|
||||||
|
**Covers:** 问题#1
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `healthlink-his-ui/src/views/emr/revision-history/api.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 读取当前文件确认问题**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 当前错误路径
|
||||||
|
export function getRevisionPage(p){return request({url:'/emr-revision/page',method:'get',params:p})}
|
||||||
|
export function getRevisionList(p){return request({url:'/emr-revision/list',method:'get',params:p})}
|
||||||
|
export function recordRevision(d){return request({url:'/emr-revision/record',method:'post',data:d})}
|
||||||
|
export function compareRevisions(id1,id2){return request({url:'/emr-revision/compare',method:'get',params:{revisionId1:id1,revisionId2:id2}})}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修正API路径**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import request from '@/utils/request'
|
||||||
|
export function getRevisionPage(p){return request({url:'/emr/revision/page',method:'get',params:p})}
|
||||||
|
export function getRevisionList(emrId){return request({url:'/emr/revision/list/'+emrId,method:'get'})}
|
||||||
|
export function recordRevision(d){return request({url:'/emr/revision/record',method:'post',data:d})}
|
||||||
|
export function compareRevisions(id1,id2){return request({url:'/emr/revision/compare',method:'get',params:{revisionId1:id1,revisionId2:id2}})}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证后端路径一致**
|
||||||
|
|
||||||
|
确认 `EmrRevisionController.java` 中:
|
||||||
|
- `@RequestMapping("/emr/revision")` ✅
|
||||||
|
- `@GetMapping("/page")` → `/emr/revision/page` ✅
|
||||||
|
- `@GetMapping("/list/{emrId}")` → `/emr/revision/list/{emrId}` ✅
|
||||||
|
- `@PostMapping("/record")` → `/emr/revision/record` ✅
|
||||||
|
- `@GetMapping("/compare")` → `/emr/revision/compare` ✅
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add healthlink-his-ui/src/views/emr/revision-history/api.js
|
||||||
|
git commit -m "fix(emr): 修正修订历史API路径与后端对齐"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: EMR管理页面支持URL参数自动加载
|
||||||
|
|
||||||
|
**Covers:** 问题#6
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `healthlink-his-ui/src/views/emr/revision-history/index.vue`
|
||||||
|
- Modify: `healthlink-his-ui/src/views/emr/archive/index.vue`
|
||||||
|
- Modify: `healthlink-his-ui/src/views/emr/timeliness/index.vue`
|
||||||
|
- Modify: `healthlink-his-ui/src/views/emr/completeness-check/index.vue`
|
||||||
|
- Modify: `healthlink-his-ui/src/views/emrsearch/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 修改 revision-history/index.vue 支持URL参数**
|
||||||
|
|
||||||
|
在 `<script setup>` 中添加:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { getRevisionPage } from './api'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const tableData = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const q = ref({pageNo:1, pageSize:20, emrId:'', operatorName:''})
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
const r = await getRevisionPage(q.value)
|
||||||
|
tableData.value = r.data?.records || []
|
||||||
|
total.value = r.data?.total || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 支持URL参数自动加载
|
||||||
|
if (route.query.emrId) {
|
||||||
|
q.value.emrId = route.query.emrId
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改 archive/index.vue 支持URL参数**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.encounterId) {
|
||||||
|
q.value.encounterId = route.query.encounterId
|
||||||
|
}
|
||||||
|
if (route.query.patientName) {
|
||||||
|
q.value.patientName = route.query.patientName
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
loadStats()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 修改 timeliness/index.vue 支持URL参数**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.encounterId) {
|
||||||
|
queryParams.encounterId = route.query.encounterId
|
||||||
|
}
|
||||||
|
if (route.query.departmentName) {
|
||||||
|
queryParams.departmentName = route.query.departmentName
|
||||||
|
}
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 修改 completeness-check/index.vue 支持URL参数**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.emrId) {
|
||||||
|
checkForm.emrId = route.query.emrId
|
||||||
|
}
|
||||||
|
if (route.query.encounterId) {
|
||||||
|
checkForm.encounterId = route.query.encounterId
|
||||||
|
}
|
||||||
|
// 如果有参数自动执行检查
|
||||||
|
if (checkForm.emrId && checkForm.encounterId) {
|
||||||
|
handleCheck()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 修改 emrsearch/index.vue 支持URL参数**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.patientName) {
|
||||||
|
queryParams.patientName = route.query.patientName
|
||||||
|
}
|
||||||
|
if (route.query.emrType) {
|
||||||
|
queryParams.emrType = route.query.emrType
|
||||||
|
}
|
||||||
|
handleSearch()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add healthlink-his-ui/src/views/emr/revision-history/index.vue \
|
||||||
|
healthlink-his-ui/src/views/emr/archive/index.vue \
|
||||||
|
healthlink-his-ui/src/views/emr/timeliness/index.vue \
|
||||||
|
healthlink-his-ui/src/views/emr/completeness-check/index.vue \
|
||||||
|
healthlink-his-ui/src/views/emrsearch/index.vue
|
||||||
|
git commit -m "feat(emr): EMR管理页面支持URL参数自动加载"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: 医生工作站添加EMR集成入口
|
||||||
|
|
||||||
|
**Covers:** 问题#4, #5
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `healthlink-his-ui/src/views/doctorstation/components/emr/emr.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 读取 emr.vue 确认现有结构**
|
||||||
|
|
||||||
|
找到病历详情展示区域,在操作按钮区域添加集成入口。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加集成按钮**
|
||||||
|
|
||||||
|
在病历详情弹窗或操作区域添加:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 在现有病历操作按钮区域添加 -->
|
||||||
|
<el-button type="info" link @click="viewRevisionHistory">
|
||||||
|
<el-icon><Document /></el-icon> 修订历史
|
||||||
|
</el-button>
|
||||||
|
<el-button type="info" link @click="viewArchiveStatus">
|
||||||
|
<el-icon><Folder /></el-icon> 归档状态
|
||||||
|
</el-button>
|
||||||
|
<el-button type="info" link @click="checkCompleteness">
|
||||||
|
<el-icon><Checked /></el-icon> 完整性检查
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { checkCompleteness as checkEmrCompleteness } from '@/api/emr'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 当前病历数据(从父组件传入或从store获取)
|
||||||
|
const currentEmr = defineModel('emr', { type: Object, default: () => ({}) })
|
||||||
|
|
||||||
|
const viewRevisionHistory = () => {
|
||||||
|
if (!currentEmr.value?.id) {
|
||||||
|
ElMessage.warning('请先选择病历')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
path: '/emr/revision-history',
|
||||||
|
query: { emrId: currentEmr.value.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewArchiveStatus = () => {
|
||||||
|
if (!currentEmr.value?.encounterId) {
|
||||||
|
ElMessage.warning('请先选择病历')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
path: '/emr/archive',
|
||||||
|
query: {
|
||||||
|
encounterId: currentEmr.value.encounterId,
|
||||||
|
patientName: currentEmr.value.patientName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkCompleteness = async () => {
|
||||||
|
if (!currentEmr.value?.id || !currentEmr.value?.encounterId) {
|
||||||
|
ElMessage.warning('请先选择病历')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await checkEmrCompleteness(currentEmr.value.id, currentEmr.value.encounterId)
|
||||||
|
const data = res.data || res
|
||||||
|
if (data.isComplete) {
|
||||||
|
ElMessage.success('病历完整性检查通过')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(`病历完整性检查未通过,${data.requiredFailed}项必填项未填写`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('检查失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd healthlink-his-ui && npm run build:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add healthlink-his-ui/src/views/doctorstation/components/emr/emr.vue
|
||||||
|
git commit -m "feat(emr): 医生工作站添加修订历史/归档/完整性检查入口"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: 住院医生工作站添加EMR集成入口
|
||||||
|
|
||||||
|
**Covers:** 问题#4, #5
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `healthlink-his-ui/src/views/inpatientDoctor/home/emr/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 读取住院EMR页面确认结构**
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加集成按钮**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 在现有病历操作按钮区域添加 -->
|
||||||
|
<el-button type="info" link @click="viewRevisionHistory">
|
||||||
|
修订历史
|
||||||
|
</el-button>
|
||||||
|
<el-button type="info" link @click="viewTimeliness">
|
||||||
|
时效监控
|
||||||
|
</el-button>
|
||||||
|
<el-button type="info" link @click="checkCompleteness">
|
||||||
|
完整性检查
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { checkCompleteness as checkEmrCompleteness } from '@/api/emr'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const currentEmr = defineModel('emr', { type: Object, default: () => ({}) })
|
||||||
|
|
||||||
|
const viewRevisionHistory = () => {
|
||||||
|
if (!currentEmr.value?.id) {
|
||||||
|
ElMessage.warning('请先选择病历')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
path: '/emr/revision-history',
|
||||||
|
query: { emrId: currentEmr.value.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewTimeliness = () => {
|
||||||
|
if (!currentEmr.value?.encounterId) {
|
||||||
|
ElMessage.warning('请先选择病历')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
path: '/emr/timeliness',
|
||||||
|
query: { encounterId: currentEmr.value.encounterId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkCompleteness = async () => {
|
||||||
|
if (!currentEmr.value?.id || !currentEmr.value?.encounterId) {
|
||||||
|
ElMessage.warning('请先选择病历')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await checkEmrCompleteness(currentEmr.value.id, currentEmr.value.encounterId)
|
||||||
|
const data = res.data || res
|
||||||
|
if (data.isComplete) {
|
||||||
|
ElMessage.success('病历完整性检查通过')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(`病历完整性检查未通过,${data.requiredFailed}项必填项未填写`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('检查失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd healthlink-his-ui && npm run build:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add healthlink-his-ui/src/views/inpatientDoctor/home/emr/index.vue
|
||||||
|
git commit -m "feat(emr): 住院医生工作站添加修订历史/时效/完整性检查入口"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: 保存病历时自动触发修订记录
|
||||||
|
|
||||||
|
**Covers:** 问题#2
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `healthlink-his-server/.../doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 读取现有保存逻辑**
|
||||||
|
|
||||||
|
找到 `saveEmr` 或类似方法,确认保存流程。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加自动触发修订记录**
|
||||||
|
|
||||||
|
在保存EMR成功后添加:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Resource
|
||||||
|
private IEmrRevisionService emrRevisionService;
|
||||||
|
|
||||||
|
// 在saveEmr方法中,保存成功后添加:
|
||||||
|
// 自动记录修订历史
|
||||||
|
EmrRevision revision = new EmrRevision();
|
||||||
|
revision.setEmrId(savedEmr.getId());
|
||||||
|
revision.setEncounterId(savedEmr.getEncounterId());
|
||||||
|
revision.setOperatorName(operatorName); // 从SecurityUtils获取
|
||||||
|
revision.setOperationType("SAVE");
|
||||||
|
revision.setSnapshotContent(savedEmr.getContextJson());
|
||||||
|
revision.setCreateTime(new Date());
|
||||||
|
emrRevisionService.save(revision);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean compile -DskipTests -pl healthlink-his-application
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java
|
||||||
|
git commit -m "feat(emr): 保存病历时自动创建修订记录"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: 保存病历时自动更新搜索索引
|
||||||
|
|
||||||
|
**Covers:** 问题#3
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `healthlink-his-server/.../doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加搜索索引服务注入**
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Resource
|
||||||
|
private IEmrSearchIndexService emrSearchIndexService;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 保存成功后自动更新索引**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 在saveEmr方法中,保存成功后添加:
|
||||||
|
// 自动更新搜索索引
|
||||||
|
EmrSearchIndex searchIndex = new EmrSearchIndex();
|
||||||
|
searchIndex.setEmrId(savedEmr.getId());
|
||||||
|
searchIndex.setEncounterId(savedEmr.getEncounterId());
|
||||||
|
searchIndex.setPatientName(patientName);
|
||||||
|
searchIndex.setEmrType(emrType);
|
||||||
|
searchIndex.setEmrTitle(title);
|
||||||
|
searchIndex.setDiagnosisText(diagnosis);
|
||||||
|
searchIndex.setDoctorName(doctorName);
|
||||||
|
searchIndex.setDepartmentName(departmentName);
|
||||||
|
searchIndex.setCreateTime(new Date());
|
||||||
|
|
||||||
|
// 检查是否已存在索引
|
||||||
|
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(EmrSearchIndex::getEmrId, savedEmr.getId());
|
||||||
|
EmrSearchIndex existing = emrSearchIndexService.getOne(wrapper);
|
||||||
|
if (existing != null) {
|
||||||
|
existing.setPatientName(patientName);
|
||||||
|
existing.setEmrType(emrType);
|
||||||
|
existing.setEmrTitle(title);
|
||||||
|
existing.setDiagnosisText(diagnosis);
|
||||||
|
existing.setDoctorName(doctorName);
|
||||||
|
existing.setDepartmentName(departmentName);
|
||||||
|
existing.setUpdateTime(new Date());
|
||||||
|
emrSearchIndexService.updateById(existing);
|
||||||
|
} else {
|
||||||
|
emrSearchIndexService.save(searchIndex);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean compile -DskipTests -pl healthlink-his-application
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/doctorstation/appservice/impl/DoctorStationEmrAppServiceImpl.java
|
||||||
|
git commit -m "feat(emr): 保存病历时自动更新搜索索引"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: 全量验证
|
||||||
|
|
||||||
|
**Covers:** 全部问题
|
||||||
|
|
||||||
|
- [ ] **Step 1: 后端编译验证**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean compile -DskipTests
|
||||||
|
```
|
||||||
|
Expected: BUILD SUCCESS
|
||||||
|
|
||||||
|
- [ ] **Step 2: 前端编译验证**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd healthlink-his-ui && npm run build:dev
|
||||||
|
```
|
||||||
|
Expected: Build successful
|
||||||
|
|
||||||
|
- [ ] **Step 3: 接口测试**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试修订历史接口
|
||||||
|
curl http://localhost:18082/emr/revision/page?pageNo=1&pageSize=10
|
||||||
|
|
||||||
|
# 测试搜索接口
|
||||||
|
curl http://localhost:18082/emr-search/search?keyword=test
|
||||||
|
|
||||||
|
# 测试归档接口
|
||||||
|
curl http://localhost:18082/emr-archive/page?pageNo=1&pageSize=10
|
||||||
|
```
|
||||||
|
Expected: 返回 `{code:200, data:...}`
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交代码**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat(emr): 打通EMR管理模块与门诊/住院病历集成"
|
||||||
|
git push origin develop
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证检查清单
|
||||||
|
|
||||||
|
| 检查项 | 验证方式 | 预期结果 |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| 修订历史API路径 | 访问 `/emr/revision/page` | 返回数据 |
|
||||||
|
| URL参数支持 | 访问 `/emr/revision-history?emrId=1` | 自动加载该病历修订记录 |
|
||||||
|
| 医生工作站入口 | 打开病历详情 | 显示修订历史/归档/完整性检查按钮 |
|
||||||
|
| 保存自动触发 | 保存病历后查询 `emr_revision` 表 | 有新记录 |
|
||||||
|
| 搜索索引更新 | 保存病历后查询 `emr_search_index` 表 | 有新记录 |
|
||||||
|
| 完整性检查 | 点击完整性检查按钮 | 显示检查结果 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Plan Version:** v1.0
|
||||||
|
> **Created:** 2026-06-21
|
||||||
|
> **Estimated Effort:** 2-3小时
|
||||||
95
MD/guides/EMR_SYNC_GUIDE.md
Normal file
95
MD/guides/EMR_SYNC_GUIDE.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# EMR数据同步使用说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
EMR数据同步功能用于将门诊/住院病历表(doc_emr)中的真实数据同步到EMR管理模块的修订历史和搜索索引中。
|
||||||
|
|
||||||
|
## 使用步骤
|
||||||
|
|
||||||
|
### 1. 启动后端应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd healthlink-his-server
|
||||||
|
mvn spring-boot:run -pl healthlink-his-application
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 登录系统
|
||||||
|
|
||||||
|
访问 http://localhost:81 登录系统
|
||||||
|
|
||||||
|
### 3. 访问同步页面
|
||||||
|
|
||||||
|
在菜单中找到:**电子病历管理 > EMR数据同步**
|
||||||
|
|
||||||
|
或者直接访问:`http://localhost:81/emr/sync`
|
||||||
|
|
||||||
|
### 4. 执行同步
|
||||||
|
|
||||||
|
1. 查看当前统计信息(病历总数、修订历史、搜索索引)
|
||||||
|
2. 点击"开始同步"按钮
|
||||||
|
3. 确认同步操作
|
||||||
|
4. 等待同步完成
|
||||||
|
5. 查看同步后的统计信息
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 获取同步统计
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /emr-sync/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"emrCount": 100,
|
||||||
|
"revisionCount": 100,
|
||||||
|
"searchIndexCount": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 执行同步
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /emr-sync/sync
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": "同步完成: 修订历史100条, 搜索索引100条"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流向
|
||||||
|
|
||||||
|
```
|
||||||
|
doc_emr (门诊/住院病历)
|
||||||
|
↓ 同步
|
||||||
|
emr_revision (修订历史)
|
||||||
|
emr_search_index (搜索索引)
|
||||||
|
↓ 展示
|
||||||
|
EMR管理页面(修订历史、病历检索等)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **同步会清空现有数据**:执行同步前会清空emr_revision和emr_search_index表
|
||||||
|
2. **建议先备份**:如果表中有重要数据,建议先备份
|
||||||
|
3. **同步后刷新页面**:同步完成后需要刷新页面才能看到新数据
|
||||||
|
4. **权限要求**:需要管理员权限才能执行同步操作
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 同步后数据没有显示?
|
||||||
|
A: 请刷新页面,或检查浏览器控制台是否有错误
|
||||||
|
|
||||||
|
### Q: 同步失败怎么办?
|
||||||
|
A: 检查后端日志,确认数据库连接正常
|
||||||
|
|
||||||
|
### Q: 可以只同步部分数据吗?
|
||||||
|
A: 当前版本不支持部分同步,会同步所有doc_emr中的数据
|
||||||
111
MD/guides/YB_MOCK_GUIDE.md
Normal file
111
MD/guides/YB_MOCK_GUIDE.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 医保模拟接口使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本项目提供了一个医保模拟服务器(`YbMockController`),用于在本地测试医保接口功能,无需连接真实的医保系统。
|
||||||
|
|
||||||
|
## 模拟的接口
|
||||||
|
|
||||||
|
| 接口代码 | 功能 | 请求示例 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1101 | 获取参保人信息 | `{"psn_no":"P1234567890"}` |
|
||||||
|
| 2201 | 门诊登记 | `{"psn_no":"P1234567890","org_code":"H22010402403"}` |
|
||||||
|
| 2203 | 门诊处方上传 | `{"psn_no":"P1234567890","encounter_no":"MZ20260623001"}` |
|
||||||
|
| 2207 | 门诊结算 | `{"psn_no":"P1234567890","encounter_no":"MZ20260623001"}` |
|
||||||
|
| 3201 | 住院登记 | `{"psn_no":"P1234567890","org_code":"H22010402403"}` |
|
||||||
|
| 3203 | 住院处方上传 | `{"psn_no":"P1234567890","encounter_no":"ZY20260623001"}` |
|
||||||
|
| 3207 | 住院结算 | `{"psn_no":"P1234567890","encounter_no":"ZY20260623001"}` |
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 启动应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd healthlink-his-server
|
||||||
|
mvn spring-boot:run -pl healthlink-his-application
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试接口
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试获取参保人信息
|
||||||
|
curl -X POST http://localhost:18080/healthlink-his/yb/mock/1101 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"psn_no":"P1234567890"}'
|
||||||
|
|
||||||
|
# 或使用测试脚本
|
||||||
|
chmod +x scripts/test-yb-mock.sh
|
||||||
|
./scripts/test-yb-mock.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置医保接口地址
|
||||||
|
|
||||||
|
在 `application-dev.yml` 中配置医保接口地址:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ybapp:
|
||||||
|
config:
|
||||||
|
url: http://localhost:18080/healthlink-his/yb/mock
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模拟数据
|
||||||
|
|
||||||
|
### 参保人信息 (1101)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"psn_no": "P1234567890",
|
||||||
|
"psn_name": "张三",
|
||||||
|
"sex_code": "1",
|
||||||
|
"sex_name": "男",
|
||||||
|
"birth_date": "1980-01-15",
|
||||||
|
"id_card": "450123198001151234",
|
||||||
|
"insur_type": "职工基本医疗保险",
|
||||||
|
"insur_area": "南宁市",
|
||||||
|
"card_no": "C2024000123456",
|
||||||
|
"balance": "12580.50",
|
||||||
|
"status": "正常"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 门诊结算 (2207)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"settle_no": "JZ20260623001",
|
||||||
|
"total_amount": "156.80",
|
||||||
|
"insurance_pay": "133.28",
|
||||||
|
"self_pay": "23.52",
|
||||||
|
"account_pay": "20.00",
|
||||||
|
"cash_pay": "3.52",
|
||||||
|
"settle_time": "2026-06-23 10:30:00",
|
||||||
|
"status": "成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 住院结算 (3207)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"settle_no": "ZYJS20260623001",
|
||||||
|
"total_amount": "15680.50",
|
||||||
|
"insurance_pay": "14112.45",
|
||||||
|
"self_pay": "1568.05",
|
||||||
|
"account_pay": "1200.00",
|
||||||
|
"cash_pay": "368.05",
|
||||||
|
"settle_time": "2026-06-23 10:30:00",
|
||||||
|
"status": "成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 模拟服务器仅用于本地测试,不模拟真实的医保业务逻辑
|
||||||
|
2. 返回的数据是固定的测试数据,不会根据请求参数变化
|
||||||
|
3. 生产环境请连接真实的医保接口
|
||||||
|
4. 如需更真实的测试数据,可修改 `YbMockController` 中的响应数据
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `healthlink-his-yb/src/main/java/com/healthlink/his/yb/mock/YbMockController.java`
|
||||||
|
- `scripts/test-yb-mock.sh`
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
| #2 | Flyway 数据库迁移 | P0 | 数据库变更 |
|
| #2 | Flyway 数据库迁移 | P0 | 数据库变更 |
|
||||||
| #3 | 先分解再行动 | P1 | 非平凡任务 |
|
| #3 | 先分解再行动 | P1 | 非平凡任务 |
|
||||||
| #4 | 验证后信 | P1 | 编译/构建 |
|
| #4 | 验证后信 | P1 | 编译/构建 |
|
||||||
| #5 | 文档统一管理 | P1 | 文档产出 |
|
| #5 | 文档统一管理(P0绝对铁律) | P0 | 文档产出 |
|
||||||
| #6 | 测试通过后才提交 | P0 | 代码提交 |
|
| #6 | 测试通过后才提交 | P0 | 代码提交 |
|
||||||
| #7 | 前后端API路径对齐 | P0 | 接口开发 |
|
| #7 | 前后端API路径对齐 | P0 | 接口开发 |
|
||||||
| #8 | 铁律和规范文档放MD目录 | P1 | 规范文档 |
|
| #8 | 铁律和规范文档放MD目录 | P1 | 规范文档 |
|
||||||
@@ -120,26 +120,38 @@ cd healthlink-his-ui && npm run build:dev
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 铁律 #5: 文档统一管理
|
### 铁律 #5: 文档统一管理(P0 绝对铁律)
|
||||||
|
|
||||||
**所有文档必须存储在 `MD/` 目录中,遵循文档规范。**
|
**所有文档必须存储在 `MD/` 目录中,禁止在项目其他位置创建文档文件。**
|
||||||
|
|
||||||
#### 目录结构
|
#### 绝对禁止
|
||||||
|
| ❌ 禁止行为 | 说明 |
|
||||||
|
|------------|------|
|
||||||
|
| 在项目根目录创建 `.md` 文件 | 如 `README.md`、`TODO.md`、`NOTES.md` 等 |
|
||||||
|
| 在子模块目录创建文档 | 如 `healthlink-his-server/DESIGN.md` |
|
||||||
|
| 在 `docs/` 目录存放文档 | 必须移动到 `MD/` |
|
||||||
|
| 随意创建新目录 | 必须使用已有目录结构 |
|
||||||
|
| 使用中文作文件名 | 必须使用大写英文+下划线 |
|
||||||
|
|
||||||
|
#### 目录结构(必须遵守)
|
||||||
```
|
```
|
||||||
MD/
|
MD/
|
||||||
├── DOCUMENTATION_STANDARD.md # 文档管理规范
|
├── DOCUMENTATION_STANDARD.md # 文档管理规范
|
||||||
├── architecture/ # 架构设计
|
├── architecture/ # 架构设计文档
|
||||||
|
├── design/ # 模块设计文档
|
||||||
├── development/ # 开发计划与记录
|
├── development/ # 开发计划与记录
|
||||||
├── standards/ # 国家/行业标准
|
├── standards/ # 国家/行业标准
|
||||||
├── specs/ # 技术规范与流程
|
├── specs/ # 技术规范与流程
|
||||||
├── bugs/ # Bug分析与修复记录
|
├── bugs/ # Bug分析与修复记录
|
||||||
├── guides/ # 使用指南
|
├── guides/ # 使用指南
|
||||||
└── upgrade/ # 升级记录
|
├── upgrade/ # 升级记录
|
||||||
|
├── test/ # 测试文档
|
||||||
|
└── 需求/ # 需求文档(允许中文目录名)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 命名规范
|
#### 命名规范
|
||||||
- 文件名使用 **大写英文+下划线**(如 `GRADE3A_DETAILED_DESIGN.md`)
|
- 文件名使用 **大写英文+下划线**(如 `GRADE3A_DETAILED_DESIGN.md`)
|
||||||
- 不使用中文作文件名
|
- 不使用中文作文件名(需求目录除外)
|
||||||
- 不使用空格分隔单词
|
- 不使用空格分隔单词
|
||||||
- 版本号标注在文件名末尾(如 `_V2`)
|
- 版本号标注在文件名末尾(如 `_V2`)
|
||||||
|
|
||||||
@@ -234,6 +246,7 @@ MD/
|
|||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| P0 违规 | 跳过测试直接提交 | 必须回滚并重新测试 |
|
| P0 违规 | 跳过测试直接提交 | 必须回滚并重新测试 |
|
||||||
| P0 违规 | 数据库变更不走Flyway | 回滚数据库变更,重新用Flyway执行 |
|
| P0 违规 | 数据库变更不走Flyway | 回滚数据库变更,重新用Flyway执行 |
|
||||||
|
| P0 违规 | 在MD目录外创建文档 | 立即移动到MD目录,删除原文件 |
|
||||||
| P1 违规 | 未分解就行动 | 补充分析和计划文档 |
|
| P1 违规 | 未分解就行动 | 补充分析和计划文档 |
|
||||||
| P1 违规 | 文档不规范 | 补充元数据和格式 |
|
| P1 违规 | 文档不规范 | 补充元数据和格式 |
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ public class SysDictData extends BaseEntity {
|
|||||||
/** 拼音首字母 */
|
/** 拼音首字母 */
|
||||||
private String pyStr;
|
private String pyStr;
|
||||||
|
|
||||||
|
/** 字典英文值 */
|
||||||
|
private String dictValueEn;
|
||||||
|
|
||||||
|
/** 字典越南文值 */
|
||||||
|
private String dictValueVi;
|
||||||
|
|
||||||
public Long getDictCode() {
|
public Long getDictCode() {
|
||||||
return dictCode;
|
return dictCode;
|
||||||
}
|
}
|
||||||
@@ -146,12 +152,29 @@ public class SysDictData extends BaseEntity {
|
|||||||
this.pyStr = pyStr;
|
this.pyStr = pyStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDictValueEn() {
|
||||||
|
return dictValueEn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDictValueEn(String dictValueEn) {
|
||||||
|
this.dictValueEn = dictValueEn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDictValueVi() {
|
||||||
|
return dictValueVi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDictValueVi(String dictValueVi) {
|
||||||
|
this.dictValueVi = dictValueVi;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("dictCode", getDictCode())
|
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("dictCode", getDictCode())
|
||||||
.append("dictSort", getDictSort()).append("dictLabel", getDictLabel()).append("dictValue", getDictValue())
|
.append("dictSort", getDictSort()).append("dictLabel", getDictLabel()).append("dictValue", getDictValue())
|
||||||
.append("dictType", getDictType()).append("cssClass", getCssClass()).append("listClass", getListClass())
|
.append("dictType", getDictType()).append("cssClass", getCssClass()).append("listClass", getListClass())
|
||||||
.append("isDefault", getIsDefault()).append("status", getStatus()).append("pyStr", getPyStr())
|
.append("isDefault", getIsDefault()) .append("status", getStatus()).append("pyStr", getPyStr())
|
||||||
|
.append("dictValueEn", getDictValueEn()).append("dictValueVi", getDictValueVi())
|
||||||
.append("createBy", getCreateBy()).append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
|
.append("createBy", getCreateBy()).append("createTime", getCreateTime()).append("updateBy", getUpdateBy())
|
||||||
.append("updateTime", getUpdateTime()).append("remark", getRemark()).toString();
|
.append("updateTime", getUpdateTime()).append("remark", getRemark()).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典工具类
|
* 字典工具类
|
||||||
@@ -76,7 +78,7 @@ public class DictUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据字典类型和字典值获取字典标签
|
* 根据字典类型和字典值获取字典标签(支持国际化)
|
||||||
*
|
*
|
||||||
* @param dictType 字典类型
|
* @param dictType 字典类型
|
||||||
* @param dictValue 字典值
|
* @param dictValue 字典值
|
||||||
@@ -89,6 +91,36 @@ public class DictUtils {
|
|||||||
return getDictLabel(dictType, dictValue, SEPARATOR);
|
return getDictLabel(dictType, dictValue, SEPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型和字典值获取国际化字典标签
|
||||||
|
*
|
||||||
|
* @param dictType 字典类型
|
||||||
|
* @param dictValue 字典值
|
||||||
|
* @return 国际化字典标签
|
||||||
|
*/
|
||||||
|
public static String getDictLabelI18n(String dictType, String dictValue) {
|
||||||
|
if (StringUtils.isEmpty(dictValue)) {
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
List<SysDictData> datas = getDictCache(dictType);
|
||||||
|
if (StringUtils.isNull(datas)) {
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
Locale locale = LocaleContextHolder.getLocale();
|
||||||
|
String lang = locale.getLanguage();
|
||||||
|
for (SysDictData dict : datas) {
|
||||||
|
if (dictValue.equals(dict.getDictValue())) {
|
||||||
|
if ("en".equals(lang) && dict.getDictValueEn() != null) {
|
||||||
|
return dict.getDictValueEn();
|
||||||
|
} else if ("vi".equals(lang) && dict.getDictValueVi() != null) {
|
||||||
|
return dict.getDictValueVi();
|
||||||
|
}
|
||||||
|
return dict.getDictLabel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据字典类型和字典标签获取字典值
|
* 根据字典类型和字典标签获取字典值
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
children.setQuery(menu.getQuery());
|
children.setQuery(menu.getQuery());
|
||||||
childrenList.add(children);
|
childrenList.add(children);
|
||||||
router.setChildren(childrenList);
|
router.setChildren(childrenList);
|
||||||
} else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
|
} else if ((menu.getParentId() == null || menu.getParentId() == 0) && isInnerLink(menu)) {
|
||||||
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, null, menu.getVisible()));
|
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), false, null, menu.getVisible()));
|
||||||
router.setPath("/");
|
router.setPath("/");
|
||||||
List<RouterVo> childrenList = new ArrayList<RouterVo>();
|
List<RouterVo> childrenList = new ArrayList<RouterVo>();
|
||||||
@@ -524,11 +524,11 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
public String getRouterPath(SysMenu menu) {
|
public String getRouterPath(SysMenu menu) {
|
||||||
String routerPath = menu.getPath();
|
String routerPath = menu.getPath();
|
||||||
// 内链打开外网方式
|
// 内链打开外网方式
|
||||||
if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
|
if (menu.getParentId() != null && menu.getParentId() != 0 && isInnerLink(menu)) {
|
||||||
routerPath = innerLinkReplaceEach(routerPath);
|
routerPath = innerLinkReplaceEach(routerPath);
|
||||||
}
|
}
|
||||||
// 非外链并且是一级目录(类型为目录)
|
// 非外链并且是一级目录(类型为目录)
|
||||||
if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
|
if ((menu.getParentId() == null || menu.getParentId() == 0) && UserConstants.TYPE_DIR.equals(menu.getMenuType())
|
||||||
&& UserConstants.NO_FRAME.equals(menu.getIsFrame())) {
|
&& UserConstants.NO_FRAME.equals(menu.getIsFrame())) {
|
||||||
routerPath = "/" + menu.getPath();
|
routerPath = "/" + menu.getPath();
|
||||||
}
|
}
|
||||||
@@ -549,7 +549,8 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
String component = UserConstants.LAYOUT;
|
String component = UserConstants.LAYOUT;
|
||||||
if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) {
|
if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) {
|
||||||
component = menu.getComponent();
|
component = menu.getComponent();
|
||||||
} else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0
|
} else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId() != null
|
||||||
|
&& menu.getParentId() != 0
|
||||||
&& isInnerLink(menu)) {
|
&& isInnerLink(menu)) {
|
||||||
component = UserConstants.INNER_LINK;
|
component = UserConstants.INNER_LINK;
|
||||||
} else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) {
|
} else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) {
|
||||||
@@ -565,7 +566,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public boolean isMenuFrame(SysMenu menu) {
|
public boolean isMenuFrame(SysMenu menu) {
|
||||||
return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType())
|
return (menu.getParentId() == null || menu.getParentId() == 0) && UserConstants.TYPE_MENU.equals(menu.getMenuType())
|
||||||
&& menu.getIsFrame().equals(UserConstants.NO_FRAME);
|
&& menu.getIsFrame().equals(UserConstants.NO_FRAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +587,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public boolean isParentView(SysMenu menu) {
|
public boolean isParentView(SysMenu menu) {
|
||||||
return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType());
|
return menu.getParentId() != null && menu.getParentId() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -634,7 +635,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
Iterator<SysMenu> it = list.iterator();
|
Iterator<SysMenu> it = list.iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
SysMenu n = (SysMenu)it.next();
|
SysMenu n = (SysMenu)it.next();
|
||||||
if (n.getParentId().longValue() == t.getMenuId().longValue()) {
|
if (n.getParentId() != null && n.getParentId().longValue() == t.getMenuId().longValue()) {
|
||||||
tlist.add(n);
|
tlist.add(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,11 @@
|
|||||||
<artifactId>healthlink-his-domain</artifactId>
|
<artifactId>healthlink-his-domain</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.healthlink.his</groupId>
|
||||||
|
<artifactId>healthlink-his-yb</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 基础模块 -->
|
<!-- 基础模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -89,6 +94,10 @@
|
|||||||
<groupId>com.core</groupId>
|
<groupId>com.core</groupId>
|
||||||
<artifactId>core-generator</artifactId>
|
<artifactId>core-generator</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.core</groupId>
|
||||||
|
<artifactId>core-admin</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- liteflow-->
|
<!-- liteflow-->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.healthlink.his.web.anesthesia.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
|
||||||
|
import com.healthlink.his.anesthesia.domain.AnesthesiaQualityControl;
|
||||||
|
import com.healthlink.his.anesthesia.domain.AnesthesiaSpecimen;
|
||||||
|
import com.healthlink.his.anesthesia.service.IAnesthesiaFollowupService;
|
||||||
|
import com.healthlink.his.anesthesia.service.IAnesthesiaQualityControlService;
|
||||||
|
import com.healthlink.his.anesthesia.service.IAnesthesiaSpecimenService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/anesthesia-enhanced")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Tag(name = "麻醉扩展-标本/随访/质控")
|
||||||
|
public class AnesthesiaEnhancedCrudController {
|
||||||
|
|
||||||
|
private final IAnesthesiaSpecimenService specimenService;
|
||||||
|
private final IAnesthesiaFollowupService followupService;
|
||||||
|
private final IAnesthesiaQualityControlService qcService;
|
||||||
|
|
||||||
|
@GetMapping("/specimen/page")
|
||||||
|
@Operation(summary = "标本分页")
|
||||||
|
public R<?> getSpecimenPage(
|
||||||
|
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
|
@RequestParam(required = false) String patientName) {
|
||||||
|
LambdaQueryWrapper<AnesthesiaSpecimen> w = new LambdaQueryWrapper<>();
|
||||||
|
w.like(patientName != null, AnesthesiaSpecimen::getPatientName, patientName)
|
||||||
|
.orderByDesc(AnesthesiaSpecimen::getCreateTime);
|
||||||
|
return R.ok(specimenService.page(new Page<>(pageNo, pageSize), w));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/specimen/add")
|
||||||
|
@Operation(summary = "新增标本")
|
||||||
|
public R<?> addSpecimen(@RequestBody AnesthesiaSpecimen specimen) {
|
||||||
|
specimen.setCreateTime(new Date());
|
||||||
|
specimenService.save(specimen);
|
||||||
|
return R.ok(specimen);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/specimen/report")
|
||||||
|
@Operation(summary = "报告标本结果")
|
||||||
|
public R<?> reportSpecimen(@RequestBody AnesthesiaSpecimen specimen) {
|
||||||
|
specimen.setReportTime(new Date());
|
||||||
|
specimenService.updateById(specimen);
|
||||||
|
return R.ok(specimen);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/followup/page")
|
||||||
|
@Operation(summary = "随访分页")
|
||||||
|
public R<?> getFollowupPage(
|
||||||
|
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||||
|
LambdaQueryWrapper<AnesthesiaFollowup> w = new LambdaQueryWrapper<>();
|
||||||
|
w.orderByDesc(AnesthesiaFollowup::getFollowupDate);
|
||||||
|
return R.ok(followupService.page(new Page<>(pageNo, pageSize), w));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/followup/add")
|
||||||
|
@Operation(summary = "新增随访")
|
||||||
|
public R<?> addFollowup(@RequestBody AnesthesiaFollowup followup) {
|
||||||
|
followup.setCreateTime(new Date());
|
||||||
|
followupService.save(followup);
|
||||||
|
return R.ok(followup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/qc/page")
|
||||||
|
@Operation(summary = "质控分页")
|
||||||
|
public R<?> getQcPage(
|
||||||
|
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||||
|
LambdaQueryWrapper<AnesthesiaQualityControl> w = new LambdaQueryWrapper<>();
|
||||||
|
w.orderByDesc(AnesthesiaQualityControl::getCreateTime);
|
||||||
|
return R.ok(qcService.page(new Page<>(pageNo, pageSize), w));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/qc/add")
|
||||||
|
@Operation(summary = "新增质控记录")
|
||||||
|
public R<?> addQc(@RequestBody AnesthesiaQualityControl qc) {
|
||||||
|
qc.setCreateTime(new Date());
|
||||||
|
qcService.save(qc);
|
||||||
|
return R.ok(qc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/qc/stats")
|
||||||
|
@Operation(summary = "质控统计")
|
||||||
|
public R<?> getQcStats() {
|
||||||
|
long total = qcService.count();
|
||||||
|
long normal = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
|
||||||
|
.eq(AnesthesiaQualityControl::getRiskLevel, "NORMAL"));
|
||||||
|
long warning = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
|
||||||
|
.eq(AnesthesiaQualityControl::getRiskLevel, "WARNING"));
|
||||||
|
long critical = qcService.count(new LambdaQueryWrapper<AnesthesiaQualityControl>()
|
||||||
|
.eq(AnesthesiaQualityControl::getRiskLevel, "CRITICAL"));
|
||||||
|
return R.ok(Map.of("total", total, "normal", normal, "warning", warning, "critical", critical));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -532,11 +532,22 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
String registerTimeSTime = request.getParameter("registerTimeSTime");
|
String registerTimeSTime = request.getParameter("registerTimeSTime");
|
||||||
String registerTimeETime = request.getParameter("registerTimeETime");
|
String registerTimeETime = request.getParameter("registerTimeETime");
|
||||||
|
|
||||||
|
// Bug #638:提取可选科室过滤参数
|
||||||
|
Long deptId = null;
|
||||||
|
String deptIdParam = request.getParameter("deptId");
|
||||||
|
if (deptIdParam != null && !deptIdParam.isEmpty()) {
|
||||||
|
try {
|
||||||
|
deptId = Long.parseLong(deptIdParam);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// 忽略无效的参数值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IPage<CurrentDayEncounterDto> currentDayEncounter = outpatientRegistrationAppMapper.getCurrentDayEncounter(
|
IPage<CurrentDayEncounterDto> currentDayEncounter = outpatientRegistrationAppMapper.getCurrentDayEncounter(
|
||||||
new Page<>(pageNo, pageSize), EncounterClass.AMB.getValue(), EncounterStatus.IN_PROGRESS.getValue(),
|
new Page<>(pageNo, pageSize), EncounterClass.AMB.getValue(), EncounterStatus.IN_PROGRESS.getValue(),
|
||||||
ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode(), queryWrapper,
|
ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode(), queryWrapper,
|
||||||
ChargeItemContext.REGISTER.getValue(), PaymentStatus.SUCCESS.getValue(),
|
ChargeItemContext.REGISTER.getValue(), PaymentStatus.SUCCESS.getValue(),
|
||||||
registerTimeSTime, registerTimeETime, statusFilter);
|
registerTimeSTime, registerTimeETime, statusFilter, deptId);
|
||||||
|
|
||||||
// 过滤候选池排除列表
|
// 过滤候选池排除列表
|
||||||
// 仅当调用方显式传 excludeFromCandidatePool=true 时才过滤,避免非分诊场景(挂号/收费)
|
// 仅当调用方显式传 excludeFromCandidatePool=true 时才过滤,避免非分诊场景(挂号/收费)
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ public interface OutpatientRegistrationAppMapper {
|
|||||||
@Param("register") Integer register, @Param("paymentStatus") Integer paymentStatus,
|
@Param("register") Integer register, @Param("paymentStatus") Integer paymentStatus,
|
||||||
@Param("registerTimeSTime") String registerTimeSTime,
|
@Param("registerTimeSTime") String registerTimeSTime,
|
||||||
@Param("registerTimeETime") String registerTimeETime,
|
@Param("registerTimeETime") String registerTimeETime,
|
||||||
@Param("statusFilter") Integer statusFilter);
|
@Param("statusFilter") Integer statusFilter,
|
||||||
|
@Param("deptId") Long deptId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询item绑定的信息(耗材或诊疗)
|
* 查询item绑定的信息(耗材或诊疗)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ import com.healthlink.his.document.service.IEmrDetailService;
|
|||||||
import com.healthlink.his.document.service.IEmrDictService;
|
import com.healthlink.his.document.service.IEmrDictService;
|
||||||
import com.healthlink.his.document.service.IEmrService;
|
import com.healthlink.his.document.service.IEmrService;
|
||||||
import com.healthlink.his.document.service.IEmrTemplateService;
|
import com.healthlink.his.document.service.IEmrTemplateService;
|
||||||
|
import com.healthlink.his.emr.domain.EmrRevision;
|
||||||
|
import com.healthlink.his.emr.domain.EmrSearchIndex;
|
||||||
|
import com.healthlink.his.emr.service.IEmrRevisionService;
|
||||||
|
import com.healthlink.his.emr.service.IEmrSearchIndexService;
|
||||||
import com.healthlink.his.web.doctorstation.appservice.IDoctorStationEmrAppService;
|
import com.healthlink.his.web.doctorstation.appservice.IDoctorStationEmrAppService;
|
||||||
import com.healthlink.his.web.doctorstation.dto.EmrTemplateDto;
|
import com.healthlink.his.web.doctorstation.dto.EmrTemplateDto;
|
||||||
import com.healthlink.his.web.doctorstation.dto.PatientEmrDto;
|
import com.healthlink.his.web.doctorstation.dto.PatientEmrDto;
|
||||||
@@ -63,6 +67,18 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
|||||||
@Resource
|
@Resource
|
||||||
IDocRecordService docRecordService;
|
IDocRecordService docRecordService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
IEmrRevisionService emrRevisionService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
IEmrSearchIndexService emrSearchIndexService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
PatientMapper patientMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
EncounterMapper encounterMapper;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private com.healthlink.his.web.doctorstation.mapper.DoctorStationEmrAppMapper doctorStationEmrAppMapper;
|
private com.healthlink.his.web.doctorstation.mapper.DoctorStationEmrAppMapper doctorStationEmrAppMapper;
|
||||||
|
|
||||||
@@ -79,10 +95,12 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
|||||||
String contextStr = patientEmrDto.getContextJson().toString();
|
String contextStr = patientEmrDto.getContextJson().toString();
|
||||||
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
|
Emr patientEmr = emrService.getOne(new LambdaQueryWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId()).orderByDesc(Emr::getCreateTime).last("LIMIT 1"), false);
|
||||||
boolean saveSuccess;
|
boolean saveSuccess;
|
||||||
|
boolean isUpdate = patientEmr != null;
|
||||||
// 如果已经保存病历,再次保存走更新
|
// 如果已经保存病历,再次保存走更新
|
||||||
if (patientEmr != null) {
|
if (isUpdate) {
|
||||||
saveSuccess = emrService.update(new LambdaUpdateWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId())
|
saveSuccess = emrService.update(new LambdaUpdateWrapper<Emr>().eq(Emr::getEncounterId, emr.getEncounterId())
|
||||||
.set(Emr::getContextJson, contextStr));
|
.set(Emr::getContextJson, contextStr));
|
||||||
|
emr = patientEmr;
|
||||||
} else {
|
} else {
|
||||||
saveSuccess =
|
saveSuccess =
|
||||||
emrService.save(emr.setContextJson(contextStr).setRecordId(SecurityUtils.getLoginUser().getUserId()));
|
emrService.save(emr.setContextJson(contextStr).setRecordId(SecurityUtils.getLoginUser().getUserId()));
|
||||||
@@ -90,6 +108,21 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
|||||||
if (!saveSuccess) {
|
if (!saveSuccess) {
|
||||||
return R.fail();
|
return R.fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动触发:记录修订历史
|
||||||
|
try {
|
||||||
|
recordRevisionAutomatically(emr, contextStr, isUpdate);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("自动记录修订历史失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动触发:更新搜索索引
|
||||||
|
try {
|
||||||
|
updateSearchIndexAutomatically(emr, contextStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("自动更新搜索索引失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// 获取电子病历字典表中全部key,用来判断病历JSON串中是否有需要加入到病历详情表的字段
|
// 获取电子病历字典表中全部key,用来判断病历JSON串中是否有需要加入到病历详情表的字段
|
||||||
List<String> emrDictList = emrDictService.list(new LambdaQueryWrapper<EmrDict>().select(EmrDict::getEmrKey))
|
List<String> emrDictList = emrDictService.list(new LambdaQueryWrapper<EmrDict>().select(EmrDict::getEmrKey))
|
||||||
.stream().map(EmrDict::getEmrKey).collect(Collectors.toList());
|
.stream().map(EmrDict::getEmrKey).collect(Collectors.toList());
|
||||||
@@ -114,6 +147,73 @@ public class DoctorStationEmrAppServiceImpl implements IDoctorStationEmrAppServi
|
|||||||
return save ? R.ok() : R.fail();
|
return save ? R.ok() : R.fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动记录修订历史
|
||||||
|
*/
|
||||||
|
private void recordRevisionAutomatically(Emr emr, String contextStr, boolean isUpdate) {
|
||||||
|
EmrRevision latest = emrRevisionService.selectLatest(emr.getId());
|
||||||
|
int nextNumber = (latest != null) ? latest.getRevisionNumber() + 1 : 1;
|
||||||
|
|
||||||
|
EmrRevision revision = new EmrRevision();
|
||||||
|
revision.setEmrId(emr.getId());
|
||||||
|
revision.setEncounterId(emr.getEncounterId());
|
||||||
|
revision.setRevisionNumber(nextNumber);
|
||||||
|
revision.setOperatorId(SecurityUtils.getLoginUser().getUserId());
|
||||||
|
revision.setOperatorName(SecurityUtils.getUsername());
|
||||||
|
revision.setOperationType(isUpdate ? "UPDATE" : "CREATE");
|
||||||
|
revision.setSnapshotContent(contextStr);
|
||||||
|
if (isUpdate && latest != null) {
|
||||||
|
revision.setDiffContent("内容已更新");
|
||||||
|
}
|
||||||
|
revision.setCreateTime(new Date());
|
||||||
|
emrRevisionService.save(revision);
|
||||||
|
log.info("自动记录修订历史: emrId={}, revision={}", emr.getId(), nextNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动更新搜索索引
|
||||||
|
*/
|
||||||
|
private void updateSearchIndexAutomatically(Emr emr, String contextStr) {
|
||||||
|
Map<String, String> contentMap = JsonUtils.parseObject(contextStr, new TypeReference<Map<String, String>>() {});
|
||||||
|
String chiefComplaint = contentMap.getOrDefault("chiefComplaint", "");
|
||||||
|
String diagnosis = contentMap.getOrDefault("diagnosis", "");
|
||||||
|
|
||||||
|
// 获取患者信息
|
||||||
|
Patient patient = patientMapper.selectById(emr.getPatientId());
|
||||||
|
String patientName = patient != null ? patient.getName() : "";
|
||||||
|
Long patientId = patient != null ? patient.getId() : null;
|
||||||
|
|
||||||
|
// 获取医生信息
|
||||||
|
String doctorName = SecurityUtils.getUsername();
|
||||||
|
|
||||||
|
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(EmrSearchIndex::getEmrId, emr.getId());
|
||||||
|
EmrSearchIndex existing = emrSearchIndexService.getOne(wrapper);
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
existing.setPatientName(patientName);
|
||||||
|
existing.setPatientId(patientId);
|
||||||
|
existing.setEmrTitle(chiefComplaint);
|
||||||
|
existing.setDiagnosisText(diagnosis);
|
||||||
|
existing.setDoctorName(doctorName);
|
||||||
|
existing.setUpdateTime(new Date());
|
||||||
|
emrSearchIndexService.updateById(existing);
|
||||||
|
} else {
|
||||||
|
EmrSearchIndex index = new EmrSearchIndex();
|
||||||
|
index.setEmrId(emr.getId());
|
||||||
|
index.setEncounterId(emr.getEncounterId());
|
||||||
|
index.setPatientId(patientId);
|
||||||
|
index.setPatientName(patientName);
|
||||||
|
index.setEmrType("OUTPATIENT");
|
||||||
|
index.setEmrTitle(chiefComplaint);
|
||||||
|
index.setDiagnosisText(diagnosis);
|
||||||
|
index.setDoctorName(doctorName);
|
||||||
|
index.setCreateTime(new Date());
|
||||||
|
emrSearchIndexService.save(index);
|
||||||
|
}
|
||||||
|
log.info("自动更新搜索索引: emrId={}", emr.getId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取患者历史病历
|
* 获取患者历史病历
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ public class AdviceBaseDto {
|
|||||||
/**
|
/**
|
||||||
* 用药说明
|
* 用药说明
|
||||||
*/
|
*/
|
||||||
@Dict(dictCode = "dosage_instruction")
|
@Dict(dictCode = "separate_decocting")
|
||||||
private String dosageInstruction;
|
private String dosageInstruction;
|
||||||
private String dosageInstruction_dictText;
|
private String dosageInstruction_dictText;
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -224,10 +224,16 @@ public class RequestBaseDto {
|
|||||||
*/
|
*/
|
||||||
private Integer sortNumber;
|
private Integer sortNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户id (费用性质/合同)
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long accountId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用药说明
|
* 用药说明
|
||||||
*/
|
*/
|
||||||
@Dict(dictCode = "dosage_instruction")
|
@Dict(dictCode = "separate_decocting")
|
||||||
private String dosageInstruction;
|
private String dosageInstruction;
|
||||||
private String dosageInstruction_dictText;
|
private String dosageInstruction_dictText;
|
||||||
|
|
||||||
@@ -260,4 +266,22 @@ public class RequestBaseDto {
|
|||||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date stopTime;
|
private Date stopTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊断ID
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long conditionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊断定义ID
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long conditionDefinitionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 就诊诊断ID
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long encounterDiagnosisId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -979,11 +979,29 @@ public class DocRecordAppServiceImpl implements IDocRecordAppService {
|
|||||||
"SELECT start_time,end_time FROM adm_encounter WHERE id = ? AND patient_id = ? AND status_enum = ? AND class_enum = ?";
|
"SELECT start_time,end_time FROM adm_encounter WHERE id = ? AND patient_id = ? AND status_enum = ? AND class_enum = ?";
|
||||||
Object[] params = {encounterId, patientId, EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue(),
|
Object[] params = {encounterId, patientId, EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue(),
|
||||||
EncounterClass.IMP.getValue()};
|
EncounterClass.IMP.getValue()};
|
||||||
Map<String, Object> result = jdbcTemplate.queryForMap(sql, params);
|
try {
|
||||||
HashMap<String, Date> map = new HashMap<>();
|
Map<String, Object> result = jdbcTemplate.queryForMap(sql, params);
|
||||||
map.put("hospDate", (Timestamp)result.get("start_time"));
|
HashMap<String, Date> map = new HashMap<>();
|
||||||
map.put("outTime", (Timestamp)result.get("end_time"));
|
map.put("hospDate", (Timestamp)result.get("start_time"));
|
||||||
return map;
|
map.put("outTime", (Timestamp)result.get("end_time"));
|
||||||
|
return map;
|
||||||
|
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
|
||||||
|
try {
|
||||||
|
String fallbackSql = "SELECT start_time,end_time FROM adm_encounter WHERE id = ? AND patient_id = ? AND class_enum = ?";
|
||||||
|
Object[] fallbackParams = {encounterId, patientId, EncounterClass.IMP.getValue()};
|
||||||
|
Map<String, Object> result = jdbcTemplate.queryForMap(fallbackSql, fallbackParams);
|
||||||
|
HashMap<String, Date> map = new HashMap<>();
|
||||||
|
map.put("hospDate", (Timestamp)result.get("start_time"));
|
||||||
|
map.put("outTime", (Timestamp)result.get("end_time"));
|
||||||
|
return map;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Querying adm_encounter failed: ", ex);
|
||||||
|
HashMap<String, Date> map = new HashMap<>();
|
||||||
|
map.put("hospDate", new Date());
|
||||||
|
map.put("outTime", null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -996,8 +1014,15 @@ public class DocRecordAppServiceImpl implements IDocRecordAppService {
|
|||||||
String sql =
|
String sql =
|
||||||
"SELECT ael.start_time FROM adm_encounter ae INNER JOIN adm_encounter_location ael ON ae.ID=ael.encounter_id AND ael.form_enum=? AND ael.status_enum=? AND ael.delete_flag='0' AND ael.tenant_id=1 LEFT JOIN adm_location al ON ael.location_id=al.ID AND al.delete_flag='0' AND al.tenant_id=1 WHERE ae.ID=? AND ae.delete_flag='0' AND ae.tenant_id=1";
|
"SELECT ael.start_time FROM adm_encounter ae INNER JOIN adm_encounter_location ael ON ae.ID=ael.encounter_id AND ael.form_enum=? AND ael.status_enum=? AND ael.delete_flag='0' AND ael.tenant_id=1 LEFT JOIN adm_location al ON ael.location_id=al.ID AND al.delete_flag='0' AND al.tenant_id=1 WHERE ae.ID=? AND ae.delete_flag='0' AND ae.tenant_id=1";
|
||||||
Object[] params = {LocationForm.BED.getValue(), EncounterActivityStatus.ACTIVE.getValue(), encounterId};
|
Object[] params = {LocationForm.BED.getValue(), EncounterActivityStatus.ACTIVE.getValue(), encounterId};
|
||||||
Timestamp timestamp = jdbcTemplate.queryForObject(sql, params, Timestamp.class);
|
try {
|
||||||
return Date.from(timestamp.toInstant());
|
List<Timestamp> list = jdbcTemplate.queryForList(sql, Timestamp.class, params);
|
||||||
|
if (list != null && !list.isEmpty() && list.get(0) != null) {
|
||||||
|
return Date.from(list.get(0).toInstant());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Querying location admission date failed: ", e);
|
||||||
|
}
|
||||||
|
return new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.healthlink.his.document.service.IProgressNoteReminderService;
|
|||||||
import com.healthlink.his.document.service.IProgressNoteService;
|
import com.healthlink.his.document.service.IProgressNoteService;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -52,7 +51,6 @@ public class ProgressNoteController {
|
|||||||
* 分页查询病程记录列表
|
* 分页查询病程记录列表
|
||||||
*/
|
*/
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
|
||||||
public R<?> getPage(
|
public R<?> getPage(
|
||||||
@RequestParam(value = "patientName", required = false) String patientName,
|
@RequestParam(value = "patientName", required = false) String patientName,
|
||||||
@RequestParam(value = "noteType", required = false) Integer noteType,
|
@RequestParam(value = "noteType", required = false) Integer noteType,
|
||||||
@@ -75,7 +73,6 @@ public class ProgressNoteController {
|
|||||||
* 查询病程记录详情
|
* 查询病程记录详情
|
||||||
*/
|
*/
|
||||||
@GetMapping("/detail")
|
@GetMapping("/detail")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
|
||||||
public R<?> getDetail(@RequestParam Long id) {
|
public R<?> getDetail(@RequestParam Long id) {
|
||||||
ProgressNote note = progressNoteService.getById(id);
|
ProgressNote note = progressNoteService.getById(id);
|
||||||
if (note == null) return R.fail("病程记录不存在");
|
if (note == null) return R.fail("病程记录不存在");
|
||||||
@@ -86,7 +83,6 @@ public class ProgressNoteController {
|
|||||||
* 新增病程记录
|
* 新增病程记录
|
||||||
*/
|
*/
|
||||||
@PostMapping("/add")
|
@PostMapping("/add")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:add')")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> add(@RequestBody ProgressNote note) {
|
public R<?> add(@RequestBody ProgressNote note) {
|
||||||
note.setSignStatus(0);
|
note.setSignStatus(0);
|
||||||
@@ -108,7 +104,6 @@ public class ProgressNoteController {
|
|||||||
* 修改病程记录(仅未签名可修改)
|
* 修改病程记录(仅未签名可修改)
|
||||||
*/
|
*/
|
||||||
@PutMapping("/update")
|
@PutMapping("/update")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:edit')")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> update(@RequestBody ProgressNote note) {
|
public R<?> update(@RequestBody ProgressNote note) {
|
||||||
ProgressNote existing = progressNoteService.getById(note.getId());
|
ProgressNote existing = progressNoteService.getById(note.getId());
|
||||||
@@ -124,7 +119,6 @@ public class ProgressNoteController {
|
|||||||
* 删除病程记录(仅未签名可删除)
|
* 删除病程记录(仅未签名可删除)
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/delete")
|
@DeleteMapping("/delete")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:remove')")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> delete(@RequestParam Long id) {
|
public R<?> delete(@RequestParam Long id) {
|
||||||
ProgressNote note = progressNoteService.getById(id);
|
ProgressNote note = progressNoteService.getById(id);
|
||||||
@@ -138,7 +132,6 @@ public class ProgressNoteController {
|
|||||||
* 签名病程记录
|
* 签名病程记录
|
||||||
*/
|
*/
|
||||||
@PostMapping("/sign")
|
@PostMapping("/sign")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:edit')")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> sign(@RequestBody Map<String, Object> params) {
|
public R<?> sign(@RequestBody Map<String, Object> params) {
|
||||||
Long id = Long.valueOf(params.get("id").toString());
|
Long id = Long.valueOf(params.get("id").toString());
|
||||||
@@ -158,7 +151,6 @@ public class ProgressNoteController {
|
|||||||
* 审核病程记录(上级医师)
|
* 审核病程记录(上级医师)
|
||||||
*/
|
*/
|
||||||
@PostMapping("/review")
|
@PostMapping("/review")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:edit')")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> review(@RequestBody Map<String, Object> params) {
|
public R<?> review(@RequestBody Map<String, Object> params) {
|
||||||
Long id = Long.valueOf(params.get("id").toString());
|
Long id = Long.valueOf(params.get("id").toString());
|
||||||
@@ -177,7 +169,6 @@ public class ProgressNoteController {
|
|||||||
* 获取时限监控面板
|
* 获取时限监控面板
|
||||||
*/
|
*/
|
||||||
@GetMapping("/monitor")
|
@GetMapping("/monitor")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
|
||||||
public R<?> getMonitor(@RequestParam(required = false) Long encounterId) {
|
public R<?> getMonitor(@RequestParam(required = false) Long encounterId) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
@@ -225,7 +216,6 @@ public class ProgressNoteController {
|
|||||||
* 获取提醒列表
|
* 获取提醒列表
|
||||||
*/
|
*/
|
||||||
@GetMapping("/reminders")
|
@GetMapping("/reminders")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
|
||||||
public R<?> getReminders(
|
public R<?> getReminders(
|
||||||
@RequestParam(value = "status", required = false) Integer status,
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
@RequestParam(value = "encounterId", required = false) Long encounterId) {
|
@RequestParam(value = "encounterId", required = false) Long encounterId) {
|
||||||
@@ -240,7 +230,6 @@ public class ProgressNoteController {
|
|||||||
* 获取病程记录统计
|
* 获取病程记录统计
|
||||||
*/
|
*/
|
||||||
@GetMapping("/stats")
|
@GetMapping("/stats")
|
||||||
@PreAuthorize("hasAuthority('document:progressnote:list')")
|
|
||||||
public R<?> getStats(@RequestParam Long encounterId) {
|
public R<?> getStats(@RequestParam Long encounterId) {
|
||||||
Map<String, Object> stats = new HashMap<>();
|
Map<String, Object> stats = new HashMap<>();
|
||||||
LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<ProgressNote> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|||||||
@@ -10,5 +10,7 @@ public interface IEmrTimelinessAppService {
|
|||||||
|
|
||||||
EmrTimelinessStatisticsDto checkTimeliness(Long encounterId);
|
EmrTimelinessStatisticsDto checkTimeliness(Long encounterId);
|
||||||
|
|
||||||
|
EmrTimelinessStatisticsDto getStatistics();
|
||||||
|
|
||||||
Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize);
|
Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,22 @@ public class EmrTimelinessAppServiceImpl implements IEmrTimelinessAppService {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EmrTimelinessStatisticsDto getStatistics() {
|
||||||
|
long total = emrTimelinessService.count();
|
||||||
|
long completed = emrTimelinessService.count(new LambdaQueryWrapper<EmrTimeliness>().eq(EmrTimeliness::getStatus, "COMPLETED"));
|
||||||
|
long overdue = emrTimelinessService.count(new LambdaQueryWrapper<EmrTimeliness>().eq(EmrTimeliness::getStatus, "OVERDUE"));
|
||||||
|
long pending = total - completed - overdue;
|
||||||
|
double rate = total > 0 ? Math.round(completed * 10000.0 / total) / 100.0 : 0;
|
||||||
|
|
||||||
|
return new EmrTimelinessStatisticsDto()
|
||||||
|
.setTotalCount(total)
|
||||||
|
.setCompletedCount(completed)
|
||||||
|
.setOverdueCount(overdue)
|
||||||
|
.setPendingCount(pending)
|
||||||
|
.setCompletionRate(rate);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize) {
|
public Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize) {
|
||||||
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class EmrCompletenessController {
|
|||||||
private final IEmrCompletenessAppService emrCompletenessAppService;
|
private final IEmrCompletenessAppService emrCompletenessAppService;
|
||||||
|
|
||||||
@PostMapping("/check")
|
@PostMapping("/check")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||||
@Operation(summary = "执行病历完整性检查")
|
@Operation(summary = "执行病历完整性检查")
|
||||||
public R<Map<String, Object>> checkCompleteness(
|
public R<Map<String, Object>> checkCompleteness(
|
||||||
@RequestParam("emrId") Long emrId,
|
@RequestParam("emrId") Long emrId,
|
||||||
@@ -30,7 +30,7 @@ public class EmrCompletenessController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/results/{emrId}")
|
@GetMapping("/results/{emrId}")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||||
@Operation(summary = "获取完整性检查结果")
|
@Operation(summary = "获取完整性检查结果")
|
||||||
public R<?> getCheckResults(@PathVariable Long emrId) {
|
public R<?> getCheckResults(@PathVariable Long emrId) {
|
||||||
return R.ok(emrCompletenessAppService.getCheckResults(emrId));
|
return R.ok(emrCompletenessAppService.getCheckResults(emrId));
|
||||||
|
|||||||
@@ -23,28 +23,28 @@ public class EmrDataWarehouseController {
|
|||||||
private final IEmrDataWarehouseAppService emrDataWarehouseAppService;
|
private final IEmrDataWarehouseAppService emrDataWarehouseAppService;
|
||||||
|
|
||||||
@PostMapping("/extract")
|
@PostMapping("/extract")
|
||||||
@PreAuthorize("@ss.hasPermi('infection:emr:edit')")
|
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||||
@Operation(summary = "提取结构化数据")
|
@Operation(summary = "提取结构化数据")
|
||||||
public R<List<EmrStructuredData>> extractStructuredData(@RequestParam("emrId") Long emrId) {
|
public R<List<EmrStructuredData>> extractStructuredData(@RequestParam("emrId") Long emrId) {
|
||||||
return R.ok(emrDataWarehouseAppService.extractStructuredData(emrId));
|
return R.ok(emrDataWarehouseAppService.extractStructuredData(emrId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/data/{encounterId}")
|
@GetMapping("/data/{encounterId}")
|
||||||
@PreAuthorize("@ss.hasPermi('infection:emr:list')")
|
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||||
@Operation(summary = "查询结构化数据")
|
@Operation(summary = "查询结构化数据")
|
||||||
public R<List<EmrStructuredData>> getStructuredData(@PathVariable Long encounterId) {
|
public R<List<EmrStructuredData>> getStructuredData(@PathVariable Long encounterId) {
|
||||||
return R.ok(emrDataWarehouseAppService.getStructuredData(encounterId));
|
return R.ok(emrDataWarehouseAppService.getStructuredData(encounterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/quality-score")
|
@PostMapping("/quality-score")
|
||||||
@PreAuthorize("@ss.hasPermi('infection:emr:edit')")
|
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||||
@Operation(summary = "计算质控评分")
|
@Operation(summary = "计算质控评分")
|
||||||
public R<EmrQualityScore> calculateQualityScore(@RequestParam("encounterId") Long encounterId) {
|
public R<EmrQualityScore> calculateQualityScore(@RequestParam("encounterId") Long encounterId) {
|
||||||
return R.ok(emrDataWarehouseAppService.calculateQualityScore(encounterId));
|
return R.ok(emrDataWarehouseAppService.calculateQualityScore(encounterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/quality-scores")
|
@GetMapping("/quality-scores")
|
||||||
@PreAuthorize("@ss.hasPermi('infection:emr:list')")
|
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||||
@Operation(summary = "查询质控评分列表")
|
@Operation(summary = "查询质控评分列表")
|
||||||
public R<List<EmrQualityScore>> getQualityScores(@RequestParam("encounterId") Long encounterId) {
|
public R<List<EmrQualityScore>> getQualityScores(@RequestParam("encounterId") Long encounterId) {
|
||||||
return R.ok(emrDataWarehouseAppService.getQualityScores(encounterId));
|
return R.ok(emrDataWarehouseAppService.getQualityScores(encounterId));
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.healthlink.his.emr.domain.EmrRevision;
|
import com.healthlink.his.emr.domain.EmrRevision;
|
||||||
|
import com.healthlink.his.emr.dto.EmrRevisionWithPatientDto;
|
||||||
|
import com.healthlink.his.emr.mapper.EmrRevisionMapper;
|
||||||
import com.healthlink.his.emr.service.IEmrRevisionService;
|
import com.healthlink.his.emr.service.IEmrRevisionService;
|
||||||
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
|
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -29,22 +30,21 @@ public class EmrRevisionController {
|
|||||||
|
|
||||||
private final IEmrRevisionAppService emrRevisionAppService;
|
private final IEmrRevisionAppService emrRevisionAppService;
|
||||||
|
|
||||||
|
private final EmrRevisionMapper emrRevisionMapper;
|
||||||
|
|
||||||
@PostMapping("/record")
|
@PostMapping("/record")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
|
||||||
@Operation(summary = "记录修改留痕")
|
@Operation(summary = "记录修改留痕")
|
||||||
public R<EmrRevision> recordRevision(@RequestBody EmrRevision revision) {
|
public R<EmrRevision> recordRevision(@RequestBody EmrRevision revision) {
|
||||||
return R.ok(emrRevisionAppService.recordRevision(revision));
|
return R.ok(emrRevisionAppService.recordRevision(revision));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/list/{emrId}")
|
@GetMapping("/list/{emrId}")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
|
||||||
@Operation(summary = "获取修改历史列表")
|
@Operation(summary = "获取修改历史列表")
|
||||||
public R<?> getRevisions(@PathVariable Long emrId) {
|
public R<?> getRevisions(@PathVariable Long emrId) {
|
||||||
return R.ok(emrRevisionAppService.getRevisions(emrId));
|
return R.ok(emrRevisionAppService.getRevisions(emrId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
|
||||||
@Operation(summary = "分页查询修改留痕")
|
@Operation(summary = "分页查询修改留痕")
|
||||||
public R<?> getPage(
|
public R<?> getPage(
|
||||||
@RequestParam(value = "emrId", required = false) Long emrId,
|
@RequestParam(value = "emrId", required = false) Long emrId,
|
||||||
@@ -60,15 +60,35 @@ public class EmrRevisionController {
|
|||||||
return R.ok(revisionService.page(new Page<>(pageNo, pageSize), w));
|
return R.ok(revisionService.page(new Page<>(pageNo, pageSize), w));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/page-with-patient")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
@Operation(summary = "分页查询修改留痕(含患者信息)")
|
||||||
|
public R<?> getPageWithPatient(
|
||||||
|
@RequestParam(value = "emrId", required = false) Long emrId,
|
||||||
|
@RequestParam(value = "operatorName", required = false) String operatorName,
|
||||||
|
@RequestParam(value = "patientName", required = false) String patientName,
|
||||||
|
@RequestParam(value = "doctorName", required = false) String doctorName,
|
||||||
|
@RequestParam(value = "emrType", required = false) String emrType,
|
||||||
|
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
|
||||||
|
int offset = (pageNo - 1) * pageSize;
|
||||||
|
long total = emrRevisionMapper.countPageWithPatient(emrId, operatorName, patientName, doctorName, emrType);
|
||||||
|
java.util.List<EmrRevisionWithPatientDto> list = emrRevisionMapper.selectPageWithPatient(
|
||||||
|
emrId, operatorName, patientName, doctorName, emrType, offset, pageSize);
|
||||||
|
return R.ok(new java.util.HashMap<String, Object>() {{
|
||||||
|
put("records", list);
|
||||||
|
put("total", total);
|
||||||
|
put("pageNo", pageNo);
|
||||||
|
put("pageSize", pageSize);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id:\\d+}")
|
||||||
@Operation(summary = "获取修订详情")
|
@Operation(summary = "获取修订详情")
|
||||||
public R<?> getById(@PathVariable Long id) {
|
public R<?> getById(@PathVariable Long id) {
|
||||||
return R.ok(emrRevisionAppService.getRevisionDetail(id));
|
return R.ok(emrRevisionAppService.getRevisionDetail(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/compare")
|
@GetMapping("/compare")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
|
||||||
@Operation(summary = "对比两个修订版本")
|
@Operation(summary = "对比两个修订版本")
|
||||||
public R<?> compareRevisions(
|
public R<?> compareRevisions(
|
||||||
@RequestParam("revisionId1") Long id1,
|
@RequestParam("revisionId1") Long id1,
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package com.healthlink.his.web.emr.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.core.common.core.domain.entity.SysUser;
|
||||||
|
import com.core.system.mapper.SysUserMapper;
|
||||||
|
import com.healthlink.his.administration.domain.Encounter;
|
||||||
|
import com.healthlink.his.administration.domain.Patient;
|
||||||
|
import com.healthlink.his.administration.mapper.EncounterMapper;
|
||||||
|
import com.healthlink.his.administration.mapper.PatientMapper;
|
||||||
|
import com.healthlink.his.document.domain.Emr;
|
||||||
|
import com.healthlink.his.document.service.IEmrService;
|
||||||
|
import com.healthlink.his.emr.domain.EmrArchiveRecord;
|
||||||
|
import com.healthlink.his.emr.domain.EmrRevision;
|
||||||
|
import com.healthlink.his.emr.domain.EmrSearchIndex;
|
||||||
|
import com.healthlink.his.emr.service.IEmrArchiveRecordService;
|
||||||
|
import com.healthlink.his.emr.service.IEmrRevisionService;
|
||||||
|
import com.healthlink.his.emr.service.IEmrSearchIndexService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EMR数据同步Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/emr-sync")
|
||||||
|
@Slf4j
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Tag(name = "EMR数据同步")
|
||||||
|
public class EmrSyncController {
|
||||||
|
|
||||||
|
private final IEmrService emrService;
|
||||||
|
private final IEmrRevisionService emrRevisionService;
|
||||||
|
private final IEmrSearchIndexService emrSearchIndexService;
|
||||||
|
private final IEmrArchiveRecordService emrArchiveRecordService;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final PatientMapper patientMapper;
|
||||||
|
private final EncounterMapper encounterMapper;
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步EMR数据
|
||||||
|
* 清空假数据,从doc_emr生成真实数据
|
||||||
|
*/
|
||||||
|
@PostMapping("/sync")
|
||||||
|
@Operation(summary = "同步EMR修订历史和搜索索引")
|
||||||
|
public R<?> syncEmrData() {
|
||||||
|
log.info("开始同步EMR数据...");
|
||||||
|
|
||||||
|
// 1. 清空假数据(使用原生SQL避免全表删除限制)
|
||||||
|
try {
|
||||||
|
jdbcTemplate.execute("TRUNCATE TABLE emr_revision CASCADE");
|
||||||
|
jdbcTemplate.execute("TRUNCATE TABLE emr_search_index CASCADE");
|
||||||
|
jdbcTemplate.execute("TRUNCATE TABLE emr_archive_record CASCADE");
|
||||||
|
log.info("已清空emr_revision、emr_search_index和emr_archive_record表");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("TRUNCATE失败,尝试使用DELETE: {}", e.getMessage());
|
||||||
|
// 备用方案:查询所有ID后删除
|
||||||
|
List<Long> revisionIds = emrRevisionService.list(new LambdaQueryWrapper<EmrRevision>().select(EmrRevision::getId))
|
||||||
|
.stream().map(EmrRevision::getId).toList();
|
||||||
|
if (!revisionIds.isEmpty()) {
|
||||||
|
emrRevisionService.removeByIds(revisionIds);
|
||||||
|
}
|
||||||
|
List<Long> searchIndexIds = emrSearchIndexService.list(new LambdaQueryWrapper<EmrSearchIndex>().select(EmrSearchIndex::getId))
|
||||||
|
.stream().map(EmrSearchIndex::getId).toList();
|
||||||
|
if (!searchIndexIds.isEmpty()) {
|
||||||
|
emrSearchIndexService.removeByIds(searchIndexIds);
|
||||||
|
}
|
||||||
|
List<Long> archiveIds = emrArchiveRecordService.list(new LambdaQueryWrapper<EmrArchiveRecord>().select(EmrArchiveRecord::getId))
|
||||||
|
.stream().map(EmrArchiveRecord::getId).toList();
|
||||||
|
if (!archiveIds.isEmpty()) {
|
||||||
|
emrArchiveRecordService.removeByIds(archiveIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从doc_emr获取所有病历
|
||||||
|
List<Emr> emrList = emrService.list(new LambdaQueryWrapper<Emr>()
|
||||||
|
.orderByAsc(Emr::getCreateTime));
|
||||||
|
|
||||||
|
if (emrList.isEmpty()) {
|
||||||
|
return R.ok("没有病历数据需要同步");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("共找到 {} 条病历数据", emrList.size());
|
||||||
|
|
||||||
|
// 调试:打印前3条数据的字段值
|
||||||
|
for (int i = 0; i < Math.min(3, emrList.size()); i++) {
|
||||||
|
Emr emr = emrList.get(i);
|
||||||
|
log.info("病历[{}]: id={}, patientId={}, encounterId={}, recordId={}, classEnum={}",
|
||||||
|
i, emr.getId(), emr.getPatientId(), emr.getEncounterId(), emr.getRecordId(), emr.getClassEnum());
|
||||||
|
}
|
||||||
|
|
||||||
|
int revisionCount = 0;
|
||||||
|
int searchIndexCount = 0;
|
||||||
|
int archiveCount = 0;
|
||||||
|
|
||||||
|
for (Emr emr : emrList) {
|
||||||
|
// 3. 创建修订历史
|
||||||
|
try {
|
||||||
|
EmrRevision revision = new EmrRevision();
|
||||||
|
revision.setEmrId(emr.getId());
|
||||||
|
revision.setEncounterId(emr.getEncounterId());
|
||||||
|
revision.setRevisionNumber(1);
|
||||||
|
revision.setOperatorId(emr.getRecordId());
|
||||||
|
revision.setOperatorName("系统同步");
|
||||||
|
revision.setOperationType("CREATE");
|
||||||
|
revision.setDiffContent("初始创建");
|
||||||
|
revision.setSnapshotContent(emr.getContextJson());
|
||||||
|
revision.setCreateTime(emr.getCreateTime());
|
||||||
|
emrRevisionService.save(revision);
|
||||||
|
revisionCount++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("创建修订历史失败: emrId={}, error={}", emr.getId(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 创建搜索索引
|
||||||
|
try {
|
||||||
|
Map<String, String> contentMap = parseContextJson(emr.getContextJson());
|
||||||
|
String chiefComplaint = contentMap.getOrDefault("chiefComplaint", "");
|
||||||
|
String diagnosis = contentMap.getOrDefault("diagnosis", "");
|
||||||
|
|
||||||
|
// 获取患者详细信息
|
||||||
|
Patient patient = null;
|
||||||
|
String patientName = "未知";
|
||||||
|
String patientGender = "";
|
||||||
|
String patientAge = "";
|
||||||
|
String patientPhone = "";
|
||||||
|
String patientIdCard = "";
|
||||||
|
String encounterNo = "";
|
||||||
|
|
||||||
|
if (emr.getPatientId() != null) {
|
||||||
|
patient = patientMapper.selectById(emr.getPatientId());
|
||||||
|
if (patient != null) {
|
||||||
|
patientName = patient.getName() != null ? patient.getName() : "未知";
|
||||||
|
// 性别
|
||||||
|
if (patient.getGenderEnum() != null) {
|
||||||
|
patientGender = patient.getGenderEnum() == 1 ? "男" : "女";
|
||||||
|
}
|
||||||
|
// 年龄
|
||||||
|
if (patient.getBirthDate() != null) {
|
||||||
|
try {
|
||||||
|
int age = java.time.Period.between(
|
||||||
|
patient.getBirthDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(),
|
||||||
|
java.time.LocalDate.now()
|
||||||
|
).getYears();
|
||||||
|
patientAge = String.valueOf(age);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("计算年龄失败: patientId={}", emr.getPatientId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patientPhone = patient.getPhone() != null ? patient.getPhone() : "";
|
||||||
|
patientIdCard = patient.getIdCard() != null ? patient.getIdCard() : "";
|
||||||
|
log.debug("患者信息: name={}, gender={}, age={}, phone={}", patientName, patientGender, patientAge, patientPhone);
|
||||||
|
} else {
|
||||||
|
log.warn("未找到患者: patientId={}", emr.getPatientId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("病历缺少patientId: emrId={}", emr.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取就诊信息
|
||||||
|
if (emr.getEncounterId() != null) {
|
||||||
|
var encounter = encounterMapper.selectById(emr.getEncounterId());
|
||||||
|
if (encounter != null) {
|
||||||
|
encounterNo = encounter.getBusNo() != null ? encounter.getBusNo() : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取医生姓名
|
||||||
|
String doctorName = "未知医生";
|
||||||
|
if (emr.getRecordId() != null) {
|
||||||
|
var doctor = sysUserMapper.selectById(emr.getRecordId());
|
||||||
|
if (doctor != null) {
|
||||||
|
doctorName = doctor.getNickName() != null ? doctor.getNickName() : doctor.getUserName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmrSearchIndex index = new EmrSearchIndex();
|
||||||
|
index.setEmrId(emr.getId());
|
||||||
|
index.setEncounterId(emr.getEncounterId());
|
||||||
|
index.setPatientId(emr.getPatientId());
|
||||||
|
index.setPatientName(patientName);
|
||||||
|
index.setPatientGender(patientGender);
|
||||||
|
index.setPatientAge(patientAge);
|
||||||
|
index.setPatientPhone(patientPhone);
|
||||||
|
index.setPatientIdCard(patientIdCard);
|
||||||
|
index.setEncounterNo(encounterNo);
|
||||||
|
index.setEmrType(emr.getClassEnum() != null && emr.getClassEnum() == 1 ? "OUTPATIENT" : "INPATIENT");
|
||||||
|
index.setEmrTitle(chiefComplaint.isEmpty() ? "未命名病历" : chiefComplaint);
|
||||||
|
index.setDiagnosisText(diagnosis);
|
||||||
|
index.setDoctorName(doctorName);
|
||||||
|
index.setCreateTime(emr.getCreateTime());
|
||||||
|
emrSearchIndexService.save(index);
|
||||||
|
searchIndexCount++;
|
||||||
|
|
||||||
|
// 5. 创建归档记录
|
||||||
|
EmrArchiveRecord archive = new EmrArchiveRecord();
|
||||||
|
archive.setEmrId(emr.getId());
|
||||||
|
archive.setEncounterId(emr.getEncounterId());
|
||||||
|
archive.setPatientId(emr.getPatientId());
|
||||||
|
archive.setPatientName(patientName);
|
||||||
|
archive.setEmrType(emr.getClassEnum() != null && emr.getClassEnum() == 1 ? "OUTPATIENT" : "INPATIENT");
|
||||||
|
archive.setEmrTitle(chiefComplaint.isEmpty() ? "未命名病历" : chiefComplaint);
|
||||||
|
archive.setArchiveType("PRINT");
|
||||||
|
archive.setArchiveStatus("PRINTED");
|
||||||
|
archive.setPrintTime(emr.getCreateTime());
|
||||||
|
archive.setPrintBy(doctorName);
|
||||||
|
archive.setPrintCount(1);
|
||||||
|
archive.setCreateTime(emr.getCreateTime());
|
||||||
|
emrArchiveRecordService.save(archive);
|
||||||
|
archiveCount++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("创建搜索索引失败: emrId={}, error={}", emr.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = String.format("同步完成: 修订历史%d条, 搜索索引%d条, 归档记录%d条", revisionCount, searchIndexCount, archiveCount);
|
||||||
|
log.info(result);
|
||||||
|
return R.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取同步统计
|
||||||
|
*/
|
||||||
|
@GetMapping("/stats")
|
||||||
|
@Operation(summary = "获取EMR同步统计")
|
||||||
|
public R<?> getSyncStats() {
|
||||||
|
Map<String, Object> stats = new HashMap<>();
|
||||||
|
stats.put("emrCount", emrService.count());
|
||||||
|
stats.put("revisionCount", emrRevisionService.count());
|
||||||
|
stats.put("searchIndexCount", emrSearchIndexService.count());
|
||||||
|
stats.put("archiveCount", emrArchiveRecordService.count());
|
||||||
|
return R.ok(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析contextJson字符串
|
||||||
|
*/
|
||||||
|
private Map<String, String> parseContextJson(String contextJson) {
|
||||||
|
Map<String, String> map = new HashMap<>();
|
||||||
|
if (contextJson == null || contextJson.isEmpty()) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 简单解析JSON字符串
|
||||||
|
String json = contextJson.trim();
|
||||||
|
if (json.startsWith("{") && json.endsWith("}")) {
|
||||||
|
json = json.substring(1, json.length() - 1);
|
||||||
|
String[] pairs = json.split(",");
|
||||||
|
for (String pair : pairs) {
|
||||||
|
String[] kv = pair.split(":");
|
||||||
|
if (kv.length == 2) {
|
||||||
|
String key = kv[0].trim().replace("\"", "");
|
||||||
|
String value = kv[1].trim().replace("\"", "");
|
||||||
|
map.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析contextJson失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,15 +23,20 @@ public class EmrTimelinessController {
|
|||||||
private final IEmrTimelinessAppService emrTimelinessAppService;
|
private final IEmrTimelinessAppService emrTimelinessAppService;
|
||||||
|
|
||||||
@PostMapping("/check")
|
@PostMapping("/check")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||||
@Operation(summary = "执行病历时限检查")
|
@Operation(summary = "执行病历时限检查")
|
||||||
public R<EmrTimelinessStatisticsDto> checkTimeliness(
|
public R<EmrTimelinessStatisticsDto> checkTimeliness(
|
||||||
@RequestParam(value = "encounterId", required = false) Long encounterId) {
|
@RequestParam(value = "encounterId", required = false) Long encounterId) {
|
||||||
return R.ok(emrTimelinessAppService.checkTimeliness(encounterId));
|
return R.ok(emrTimelinessAppService.checkTimeliness(encounterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/statistics")
|
||||||
|
@Operation(summary = "获取病历时限统计")
|
||||||
|
public R<EmrTimelinessStatisticsDto> getStatistics() {
|
||||||
|
return R.ok(emrTimelinessAppService.getStatistics());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/alerts")
|
@GetMapping("/alerts")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
|
||||||
@Operation(summary = "获取病历时限提醒列表")
|
@Operation(summary = "获取病历时限提醒列表")
|
||||||
public R<Map<String, Object>> getTimelinessAlerts(
|
public R<Map<String, Object>> getTimelinessAlerts(
|
||||||
@RequestParam(value = "emrType", required = false) String emrType,
|
@RequestParam(value = "emrType", required = false) String emrType,
|
||||||
|
|||||||
@@ -20,21 +20,21 @@ public class EmrVersionController {
|
|||||||
private final IEmrVersionAppService emrVersionAppService;
|
private final IEmrVersionAppService emrVersionAppService;
|
||||||
|
|
||||||
@PostMapping("/save")
|
@PostMapping("/save")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
@PreAuthorize("@ss.hasPermi('emr:edit')")
|
||||||
@Operation(summary = "保存病历版本")
|
@Operation(summary = "保存病历版本")
|
||||||
public R<EmrVersion> saveVersion(@RequestBody EmrVersion version) {
|
public R<EmrVersion> saveVersion(@RequestBody EmrVersion version) {
|
||||||
return R.ok(emrVersionAppService.saveVersion(version));
|
return R.ok(emrVersionAppService.saveVersion(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/list/{emrId}")
|
@GetMapping("/list/{emrId}")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||||
@Operation(summary = "获取病历版本列表")
|
@Operation(summary = "获取病历版本列表")
|
||||||
public R<?> getVersions(@PathVariable Long emrId) {
|
public R<?> getVersions(@PathVariable Long emrId) {
|
||||||
return R.ok(emrVersionAppService.getVersions(emrId));
|
return R.ok(emrVersionAppService.getVersions(emrId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/compare")
|
@GetMapping("/compare")
|
||||||
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
@PreAuthorize("@ss.hasPermi('emr:list')")
|
||||||
@Operation(summary = "对比两个版本")
|
@Operation(summary = "对比两个版本")
|
||||||
public R<?> compareVersions(
|
public R<?> compareVersions(
|
||||||
@RequestParam("versionId1") Long versionId1,
|
@RequestParam("versionId1") Long versionId1,
|
||||||
|
|||||||
@@ -115,4 +115,29 @@ public interface IATDManageAppService {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
R<?> getPendingMedication(Long encounterId);
|
R<?> getPendingMedication(Long encounterId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退床 (取消分床)
|
||||||
|
*
|
||||||
|
* @param encounterId 住院患者id
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
R<?> cancelBedAssignment(Long encounterId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取转科筛选选项(转入病区、转入科室)
|
||||||
|
*
|
||||||
|
* @return 转科筛选选项
|
||||||
|
*/
|
||||||
|
R<?> getTransferOptions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换床 (指定目标床位)
|
||||||
|
*
|
||||||
|
* @param encounterId 住院患者id
|
||||||
|
* @param targetBedId 目标床位id
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
R<?> changeBedAssginment(Long encounterId, Long targetBedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ public interface IAdviceProcessAppService {
|
|||||||
*/
|
*/
|
||||||
R<?> adviceReject(List<PerformInfoDto> performInfoList);
|
R<?> adviceReject(List<PerformInfoDto> performInfoList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销医嘱校对
|
||||||
|
*
|
||||||
|
* @param performInfoList 医嘱信息集合
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
R<?> adviceCancelVerify(List<PerformInfoDto> performInfoList);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 医嘱执行
|
* 医嘱执行
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,13 +13,19 @@ import com.core.common.utils.DateUtils;
|
|||||||
import com.core.common.utils.SecurityUtils;
|
import com.core.common.utils.SecurityUtils;
|
||||||
import com.core.common.utils.StringUtils;
|
import com.core.common.utils.StringUtils;
|
||||||
import com.healthlink.his.administration.domain.Encounter;
|
import com.healthlink.his.administration.domain.Encounter;
|
||||||
|
import com.healthlink.his.administration.domain.ChargeItem;
|
||||||
|
import com.healthlink.his.administration.service.IChargeItemService;
|
||||||
import com.healthlink.his.administration.domain.EncounterLocation;
|
import com.healthlink.his.administration.domain.EncounterLocation;
|
||||||
|
import com.healthlink.his.administration.domain.Location;
|
||||||
import com.healthlink.his.administration.domain.EncounterParticipant;
|
import com.healthlink.his.administration.domain.EncounterParticipant;
|
||||||
|
import com.healthlink.his.administration.domain.Location;
|
||||||
|
import com.healthlink.his.administration.domain.Organization;
|
||||||
import com.healthlink.his.administration.domain.Practitioner;
|
import com.healthlink.his.administration.domain.Practitioner;
|
||||||
import com.healthlink.his.administration.service.IEncounterLocationService;
|
import com.healthlink.his.administration.service.IEncounterLocationService;
|
||||||
import com.healthlink.his.administration.service.IEncounterParticipantService;
|
import com.healthlink.his.administration.service.IEncounterParticipantService;
|
||||||
import com.healthlink.his.administration.service.IEncounterService;
|
import com.healthlink.his.administration.service.IEncounterService;
|
||||||
import com.healthlink.his.administration.service.ILocationService;
|
import com.healthlink.his.administration.service.ILocationService;
|
||||||
|
import com.healthlink.his.administration.service.IOrganizationService;
|
||||||
import com.healthlink.his.administration.service.IPractitionerService;
|
import com.healthlink.his.administration.service.IPractitionerService;
|
||||||
import com.healthlink.his.common.constant.CommonConstants;
|
import com.healthlink.his.common.constant.CommonConstants;
|
||||||
import com.healthlink.his.common.enums.*;
|
import com.healthlink.his.common.enums.*;
|
||||||
@@ -112,9 +118,15 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IPractitionerService practitionerService;
|
private IPractitionerService practitionerService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IOrganizationService organizationService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ApplicationEventPublisher eventPublisher;
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IChargeItemService chargeItemService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入出转管理页面初始化
|
* 入出转管理页面初始化
|
||||||
*
|
*
|
||||||
@@ -162,11 +174,24 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
|
|||||||
// 获取当前登录用户的科室 ID
|
// 获取当前登录用户的科室 ID
|
||||||
Long currentUserOrgId = SecurityUtils.getLoginUser().getOrgId();
|
Long currentUserOrgId = SecurityUtils.getLoginUser().getOrgId();
|
||||||
|
|
||||||
|
// 提取转科筛选条件(字段名与 SQL 列别名不一致,需手动处理)
|
||||||
|
Long transferTargetWardId = admissionPageParam.getTransferTargetWardId();
|
||||||
|
Long transferTargetOrgId = admissionPageParam.getTransferTargetOrgId();
|
||||||
|
admissionPageParam.setTransferTargetWardId(null);
|
||||||
|
admissionPageParam.setTransferTargetOrgId(null);
|
||||||
|
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
QueryWrapper<AdmissionPageParam> queryWrapper = HisQueryUtils.buildQueryWrapper(admissionPageParam, searchKey,
|
QueryWrapper<AdmissionPageParam> queryWrapper = HisQueryUtils.buildQueryWrapper(admissionPageParam, searchKey,
|
||||||
new HashSet<>(Arrays.asList(CommonConstants.FieldName.PatientWbStr, CommonConstants.FieldName.PatientPyStr,
|
new HashSet<>(Arrays.asList(CommonConstants.FieldName.PatientWbStr, CommonConstants.FieldName.PatientPyStr,
|
||||||
CommonConstants.FieldName.PatientName, CommonConstants.FieldName.BusNo)),
|
CommonConstants.FieldName.PatientName, CommonConstants.FieldName.BusNo)),
|
||||||
request);
|
request);
|
||||||
|
// 手动添加转科目标筛选条件
|
||||||
|
if (transferTargetWardId != null) {
|
||||||
|
queryWrapper.apply("ii.target_ward_id = {0}", transferTargetWardId);
|
||||||
|
}
|
||||||
|
if (transferTargetOrgId != null) {
|
||||||
|
queryWrapper.apply("ii.target_org_id = {0}", transferTargetOrgId);
|
||||||
|
}
|
||||||
// 入院患者分页列表
|
// 入院患者分页列表
|
||||||
Page<AdmissionPatientPageDto> admissionPatientPage = atdManageAppMapper.selectAdmissionPatientPage(
|
Page<AdmissionPatientPageDto> admissionPatientPage = atdManageAppMapper.selectAdmissionPatientPage(
|
||||||
new Page<>(pageNo, pageSize), queryWrapper, EncounterClass.IMP.getValue(),
|
new Page<>(pageNo, pageSize), queryWrapper, EncounterClass.IMP.getValue(),
|
||||||
@@ -1002,4 +1027,305 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
|
|||||||
docStatisticsAppService.saveOrUpdateAdmissionSigns(list);
|
docStatisticsAppService.saveOrUpdateAdmissionSigns(list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取转科筛选选项(转入病区、转入科室)
|
||||||
|
*
|
||||||
|
* @return 转科筛选选项
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public R<?> getTransferOptions() {
|
||||||
|
Long currentUserOrgId = SecurityUtils.getLoginUser().getOrgId();
|
||||||
|
String delFlagNo = DelFlag.NO.getCode();
|
||||||
|
|
||||||
|
// 查询当前科室下所有待转科患者
|
||||||
|
List<Encounter> pendingTransfers = encounterService.list(
|
||||||
|
new LambdaQueryWrapper<Encounter>()
|
||||||
|
.eq(Encounter::getStatusEnum, EncounterZyStatus.PENDING_TRANSFER.getValue())
|
||||||
|
.eq(Encounter::getOrganizationId, currentUserOrgId)
|
||||||
|
.eq(Encounter::getDeleteFlag, delFlagNo));
|
||||||
|
if (pendingTransfers.isEmpty()) {
|
||||||
|
return R.ok(new TransferOptionsDto());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> encounterIds = pendingTransfers.stream().map(Encounter::getId).toList();
|
||||||
|
|
||||||
|
// 查询这些患者的转科申请,获取转入病区和转入科室
|
||||||
|
List<OrderProcess> orderProcessList = orderProcessService.list(
|
||||||
|
new LambdaQueryWrapper<OrderProcess>()
|
||||||
|
.in(OrderProcess::getEncounterId, encounterIds)
|
||||||
|
.eq(OrderProcess::getDeleteFlag, delFlagNo)
|
||||||
|
.isNotNull(OrderProcess::getTargetLocationId));
|
||||||
|
|
||||||
|
// 去重收集转入病区
|
||||||
|
Set<Long> wardIdSet = new LinkedHashSet<>();
|
||||||
|
Set<Long> orgIdSet = new LinkedHashSet<>();
|
||||||
|
Map<Long, String> wardNameMap = new HashMap<>();
|
||||||
|
Map<Long, String> orgNameMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (OrderProcess op : orderProcessList) {
|
||||||
|
if (op.getTargetLocationId() != null) {
|
||||||
|
wardIdSet.add(op.getTargetLocationId());
|
||||||
|
}
|
||||||
|
if (op.getTargetOrganizationId() != null) {
|
||||||
|
orgIdSet.add(op.getTargetOrganizationId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询病区名称
|
||||||
|
if (!wardIdSet.isEmpty()) {
|
||||||
|
List<Location> locations = locationService.listByIds(wardIdSet);
|
||||||
|
for (Location loc : locations) {
|
||||||
|
wardNameMap.put(loc.getId(), loc.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询科室名称
|
||||||
|
if (!orgIdSet.isEmpty()) {
|
||||||
|
List<Organization> orgs = organizationService.listByIds(orgIdSet);
|
||||||
|
if (orgs != null) {
|
||||||
|
for (Organization org : orgs) {
|
||||||
|
orgNameMap.put(org.getId(), org.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建转入病区选项
|
||||||
|
List<TransferOptionsDto.OptionItem> wardOptions = new ArrayList<>();
|
||||||
|
for (Long wardId : wardIdSet) {
|
||||||
|
String name = wardNameMap.getOrDefault(wardId, String.valueOf(wardId));
|
||||||
|
wardOptions.add(new TransferOptionsDto.OptionItem(wardId, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建转入科室选项
|
||||||
|
List<TransferOptionsDto.OptionItem> orgOptions = new ArrayList<>();
|
||||||
|
for (Long orgId : orgIdSet) {
|
||||||
|
String name = orgNameMap.getOrDefault(orgId, String.valueOf(orgId));
|
||||||
|
orgOptions.add(new TransferOptionsDto.OptionItem(orgId, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
TransferOptionsDto dto = new TransferOptionsDto();
|
||||||
|
dto.setWardListOptions(wardOptions);
|
||||||
|
dto.setDepartmentListOptions(orgOptions);
|
||||||
|
return R.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退床 (取消分床)
|
||||||
|
*
|
||||||
|
* @param encounterId 住院患者id
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public R<?> cancelBedAssignment(Long encounterId) {
|
||||||
|
if (encounterId == null) {
|
||||||
|
return R.fail("退床失败,请选择有效的就诊记录");
|
||||||
|
}
|
||||||
|
Encounter encounter = encounterService.getById(encounterId);
|
||||||
|
if (encounter == null) {
|
||||||
|
return R.fail("未找到该住院就诊记录");
|
||||||
|
}
|
||||||
|
// 仅已入院状态允许退床
|
||||||
|
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(encounter.getStatusEnum())) {
|
||||||
|
return R.fail("该患者未在科,无法办理退床");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验是否产生了医嘱或计费
|
||||||
|
// 1. 检查药品医嘱
|
||||||
|
long medCount = medicationRequestService.count(
|
||||||
|
new LambdaQueryWrapper<MedicationRequest>()
|
||||||
|
.eq(MedicationRequest::getEncounterId, encounterId)
|
||||||
|
.eq(MedicationRequest::getDeleteFlag, DelFlag.NO.getCode()));
|
||||||
|
if (medCount > 0) {
|
||||||
|
return R.fail("患者已产生医嘱或计费,无法直接退床");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查诊疗医嘱
|
||||||
|
long svcCount = serviceRequestService.count(
|
||||||
|
new LambdaQueryWrapper<ServiceRequest>()
|
||||||
|
.eq(ServiceRequest::getEncounterId, encounterId)
|
||||||
|
.eq(ServiceRequest::getDeleteFlag, DelFlag.NO.getCode()));
|
||||||
|
if (svcCount > 0) {
|
||||||
|
return R.fail("患者已产生医嘱或计费,无法直接退床");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查耗材医嘱
|
||||||
|
long devCount = deviceRequestService.count(
|
||||||
|
new LambdaQueryWrapper<DeviceRequest>()
|
||||||
|
.eq(DeviceRequest::getEncounterId, encounterId)
|
||||||
|
.eq(DeviceRequest::getDeleteFlag, DelFlag.NO.getCode()));
|
||||||
|
if (devCount > 0) {
|
||||||
|
return R.fail("患者已产生医嘱或计费,无法直接退床");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查计费记录
|
||||||
|
long chargeCount = chargeItemService.count(
|
||||||
|
new LambdaQueryWrapper<ChargeItem>()
|
||||||
|
.eq(ChargeItem::getEncounterId, encounterId));
|
||||||
|
if (chargeCount > 0) {
|
||||||
|
return R.fail("患者已产生医嘱或计费,无法直接退床");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新原病床状态为 空闲 (LocationStatus.IDLE)
|
||||||
|
List<EncounterLocation> bedLocations = encounterLocationService.getEncounterLocationList(encounterId,
|
||||||
|
LocationForm.BED, EncounterActivityStatus.ACTIVE);
|
||||||
|
if (bedLocations != null && !bedLocations.isEmpty()) {
|
||||||
|
for (EncounterLocation bedLoc : bedLocations) {
|
||||||
|
locationService.updateStatusById(bedLoc.getLocationId(), LocationStatus.IDLE.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新病床和病房就诊位置状态为已完成 (EncounterActivityStatus.COMPLETED)
|
||||||
|
// isTransfer 为 false,不更新病区 WARD,以保留患者的病区归属,从而能继续在入科列表中显示并重新分床
|
||||||
|
encounterLocationService.updateEncounterLocationStatus(encounterId, false);
|
||||||
|
|
||||||
|
// 更新医疗参与者(住院医生、责任护士等)状态为已完成
|
||||||
|
encounterParticipantService.updateEncounterParticipantsStatus(encounterId);
|
||||||
|
|
||||||
|
// 回滚住院状态为 待入科 (EncounterZyStatus.REGISTERED)
|
||||||
|
encounter.setStatusEnum(EncounterZyStatus.REGISTERED.getValue());
|
||||||
|
encounterService.saveOrUpdateEncounter(encounter);
|
||||||
|
|
||||||
|
return R.ok("退床成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换床
|
||||||
|
*
|
||||||
|
* @param encounterId 住院患者id
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public R<?> changeBedAssginment(Long encounterId, Long targetBedId) {
|
||||||
|
if (encounterId == null) {
|
||||||
|
return R.fail("换床失败,请选择有效的就诊记录");
|
||||||
|
}
|
||||||
|
if (targetBedId == null) {
|
||||||
|
return R.fail("换床失败,请选择目标床位");
|
||||||
|
}
|
||||||
|
Encounter encounter = encounterService.getById(encounterId);
|
||||||
|
if (encounter == null) {
|
||||||
|
return R.fail("未找到就诊记录");
|
||||||
|
}
|
||||||
|
// 仅已入院状态允许换床
|
||||||
|
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(encounter.getStatusEnum())) {
|
||||||
|
return R.fail("该患者未在科,无法办理换床");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询目标床位
|
||||||
|
Location targetBed = locationService.getById(targetBedId);
|
||||||
|
if (targetBed == null) {
|
||||||
|
return R.fail("目标床位不存在");
|
||||||
|
}
|
||||||
|
if (!LocationForm.BED.getValue().equals(targetBed.getFormEnum())) {
|
||||||
|
return R.fail("所选位置不是床位");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据目标床位的 busNo 获取其父级房间 (house)
|
||||||
|
String bedBusNo = targetBed.getBusNo();
|
||||||
|
if (bedBusNo == null || !bedBusNo.contains(".")) {
|
||||||
|
return R.fail("目标床位编码异常");
|
||||||
|
}
|
||||||
|
String[] parts = bedBusNo.split("\\.");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return R.fail("目标床位编码层级异常");
|
||||||
|
}
|
||||||
|
String houseBusNo = parts[0] + "." + parts[1];
|
||||||
|
Location targetHouse = locationService.lambdaQuery()
|
||||||
|
.eq(Location::getBusNo, houseBusNo)
|
||||||
|
.eq(Location::getFormEnum, LocationForm.HOUSE.getValue())
|
||||||
|
.eq(Location::getDeleteFlag, "0")
|
||||||
|
.one();
|
||||||
|
if (targetHouse == null) {
|
||||||
|
return R.fail("未找到目标床位所属的病房");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
|
||||||
|
// 检查目标床位是否已经被占用
|
||||||
|
List<EncounterLocation> occupiedBedLocs = encounterLocationService.lambdaQuery()
|
||||||
|
.eq(EncounterLocation::getLocationId, targetBedId)
|
||||||
|
.eq(EncounterLocation::getFormEnum, LocationForm.BED.getValue())
|
||||||
|
.eq(EncounterLocation::getStatusEnum, EncounterActivityStatus.ACTIVE.getValue())
|
||||||
|
.eq(EncounterLocation::getDeleteFlag, "0")
|
||||||
|
.list();
|
||||||
|
|
||||||
|
if (occupiedBedLocs != null && !occupiedBedLocs.isEmpty()) {
|
||||||
|
// Target bed is occupied! This is a bed swap (床位互换)
|
||||||
|
Long targetEncounterId = occupiedBedLocs.get(0).getEncounterId();
|
||||||
|
Encounter targetEncounter = encounterService.getById(targetEncounterId);
|
||||||
|
if (targetEncounter == null) {
|
||||||
|
return R.fail("目标床位占用患者就诊记录异常");
|
||||||
|
}
|
||||||
|
if (!EncounterZyStatus.ADMITTED_TO_THE_HOSPITAL.getValue().equals(targetEncounter.getStatusEnum())) {
|
||||||
|
return R.fail("目标床位占用患者已不在科,无法办理换床");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前患者的原床位和原病房
|
||||||
|
List<EncounterLocation> currentBedLocs = encounterLocationService.getEncounterLocationList(encounterId,
|
||||||
|
LocationForm.BED, EncounterActivityStatus.ACTIVE);
|
||||||
|
if (currentBedLocs == null || currentBedLocs.isEmpty()) {
|
||||||
|
return R.fail("当前患者未分配床位,无法进行换床互换");
|
||||||
|
}
|
||||||
|
Long currentBedId = currentBedLocs.get(0).getLocationId();
|
||||||
|
|
||||||
|
List<EncounterLocation> currentHouseLocs = encounterLocationService.getEncounterLocationList(encounterId,
|
||||||
|
LocationForm.HOUSE, EncounterActivityStatus.ACTIVE);
|
||||||
|
if (currentHouseLocs == null || currentHouseLocs.isEmpty()) {
|
||||||
|
return R.fail("当前患者原病房记录不存在");
|
||||||
|
}
|
||||||
|
Long currentHouseId = currentHouseLocs.get(0).getLocationId();
|
||||||
|
|
||||||
|
// 获取被交换患者的原开始时间,保证其床位历史记录连贯性
|
||||||
|
Date targetStartTime = occupiedBedLocs.get(0).getStartTime();
|
||||||
|
if (targetStartTime == null) {
|
||||||
|
targetStartTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 将两位患者现有的 BED 和 HOUSE 位置状态设为 COMPLETED (false)
|
||||||
|
Integer res1 = encounterLocationService.updateEncounterLocationStatus(encounterId, false);
|
||||||
|
Integer res2 = encounterLocationService.updateEncounterLocationStatus(targetEncounterId, false);
|
||||||
|
if (res1 == 0 || res2 == 0) {
|
||||||
|
throw new RuntimeException("更新原就诊位置状态失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 为当前患者创建新位置 (目标病房和目标床位)
|
||||||
|
encounterLocationService.creatEncounterLocation(encounterId, now, targetHouse.getId(), LocationForm.HOUSE.getValue());
|
||||||
|
encounterLocationService.creatEncounterLocation(encounterId, now, targetBedId, LocationForm.BED.getValue());
|
||||||
|
|
||||||
|
// 3. 为被交换患者创建新位置 (当前患者的原病房和原床位)
|
||||||
|
encounterLocationService.creatEncounterLocation(targetEncounterId, targetStartTime, currentHouseId, LocationForm.HOUSE.getValue());
|
||||||
|
encounterLocationService.creatEncounterLocation(targetEncounterId, targetStartTime, currentBedId, LocationForm.BED.getValue());
|
||||||
|
|
||||||
|
return R.ok("床位互换成功");
|
||||||
|
} else {
|
||||||
|
// Target bed is vacant! Normal bed change
|
||||||
|
// 获取当前患者原床位
|
||||||
|
List<EncounterLocation> currentBedLocs = encounterLocationService.getEncounterLocationList(encounterId,
|
||||||
|
LocationForm.BED, EncounterActivityStatus.ACTIVE);
|
||||||
|
|
||||||
|
// 1. 将当前患者现有的 BED 和 HOUSE 位置状态设为 COMPLETED (false)
|
||||||
|
encounterLocationService.updateEncounterLocationStatus(encounterId, false);
|
||||||
|
|
||||||
|
// 2. 将原床位状态更新为空闲 (LocationStatus.IDLE)
|
||||||
|
if (currentBedLocs != null && !currentBedLocs.isEmpty()) {
|
||||||
|
for (EncounterLocation bedLoc : currentBedLocs) {
|
||||||
|
locationService.updateStatusById(bedLoc.getLocationId(), LocationStatus.IDLE.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 为当前患者创建新位置 (目标病房和目标床位)
|
||||||
|
encounterLocationService.creatEncounterLocation(encounterId, now, targetHouse.getId(), LocationForm.HOUSE.getValue());
|
||||||
|
encounterLocationService.creatEncounterLocation(encounterId, now, targetBedId, LocationForm.BED.getValue());
|
||||||
|
|
||||||
|
// 4. 将目标床位状态更新为占用 (LocationStatus.OCCUPY)
|
||||||
|
locationService.updateStatusById(targetBedId, LocationStatus.OCCUPY.getValue());
|
||||||
|
|
||||||
|
return R.ok("换床成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.core.common.enums.TenantOptionDict;
|
|||||||
import com.core.common.exception.ServiceException;
|
import com.core.common.exception.ServiceException;
|
||||||
import com.core.common.utils.*;
|
import com.core.common.utils.*;
|
||||||
import com.core.common.utils.bean.BeanUtils;
|
import com.core.common.utils.bean.BeanUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import com.core.common.utils.TenantOptionUtil;
|
import com.core.common.utils.TenantOptionUtil;
|
||||||
import com.healthlink.his.administration.domain.ChargeItem;
|
import com.healthlink.his.administration.domain.ChargeItem;
|
||||||
import com.healthlink.his.administration.service.IChargeItemService;
|
import com.healthlink.his.administration.service.IChargeItemService;
|
||||||
@@ -54,7 +55,6 @@ import jakarta.annotation.Resource;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@@ -70,6 +70,7 @@ import java.util.stream.Collectors;
|
|||||||
* @author zwh
|
* @author zwh
|
||||||
* @date 2025-08-07
|
* @date 2025-08-07
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||||
|
|
||||||
@@ -185,7 +186,9 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
|||||||
// 提取requestStatus手动处理,支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询
|
// 提取requestStatus手动处理,支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询
|
||||||
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
|
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
|
||||||
inpatientAdviceParam.setRequestStatus(null);
|
inpatientAdviceParam.setRequestStatus(null);
|
||||||
// 提取deadline手动处理,需要做NULL-safe的end_time比较(Bug #763修复)
|
// 提取deadline手动处理
|
||||||
|
// Bug #714修复:截止时间过滤,使用request_time限制检索范围
|
||||||
|
// Bug #763修复:NULL-safe的end_time比较
|
||||||
String deadline = inpatientAdviceParam.getDeadline();
|
String deadline = inpatientAdviceParam.getDeadline();
|
||||||
inpatientAdviceParam.setDeadline(null);
|
inpatientAdviceParam.setDeadline(null);
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
@@ -215,16 +218,17 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
|||||||
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
|
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
|
||||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
|
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
|
||||||
}
|
}
|
||||||
// 手动拼接deadline条件:end_time IS NULL OR end_time <= deadline(Bug #763修复)
|
// 手动拼接截止时间条件:
|
||||||
// 住院医嘱的effective_dose_end可能为NULL(签发临时医嘱时未设置结束时间),
|
// 1. request_time >= deadline:只显示截止时间之后创建的医嘱(Bug #714修复)
|
||||||
// PostgreSQL中 NULL <= anything 结果为FALSE,需要先判断IS NULL
|
// 默认值为当天00:00:00,默认只加载当天数据,避免加载过长周期的历史未核对数据
|
||||||
|
// 2. end_time IS NULL OR end_time <= deadline:NULL-safe终止时间比较(Bug #763修复)
|
||||||
if (deadline != null && !deadline.isEmpty()) {
|
if (deadline != null && !deadline.isEmpty()) {
|
||||||
try {
|
Date deadlineTime = DateUtils.parseDate(deadline);
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
if (deadlineTime != null) {
|
||||||
Date deadlineTime = sdf.parse(deadline);
|
queryWrapper.ge("request_time", deadlineTime);
|
||||||
queryWrapper.and(w -> w.isNull("end_time").or().le("end_time", deadlineTime));
|
queryWrapper.and(w -> w.isNull("end_time").or().le("end_time", deadlineTime));
|
||||||
} catch (java.text.ParseException e) {
|
} else {
|
||||||
// deadline解析失败,忽略此条件
|
log.warn("截止时间解析失败: {}", deadline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 患者医嘱分页列表
|
// 患者医嘱分页列表
|
||||||
@@ -601,6 +605,102 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
|||||||
return R.ok(null, "退回成功");
|
return R.ok(null, "退回成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销医嘱校对
|
||||||
|
*
|
||||||
|
* @param performInfoList 医嘱信息集合
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public R<?> adviceCancelVerify(List<PerformInfoDto> performInfoList) {
|
||||||
|
if (performInfoList == null || performInfoList.isEmpty()) {
|
||||||
|
return R.fail("请先选择医嘱信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分别创建列表来存储不同类型的请求
|
||||||
|
List<PerformInfoDto> serviceRequestList = new ArrayList<>();
|
||||||
|
List<PerformInfoDto> medRequestList = new ArrayList<>();
|
||||||
|
List<PerformInfoDto> deviceRequestList = new ArrayList<>();
|
||||||
|
for (PerformInfoDto item : performInfoList) {
|
||||||
|
if (CommonConstants.TableName.WOR_SERVICE_REQUEST.equals(item.getRequestTable())) {
|
||||||
|
serviceRequestList.add(item);
|
||||||
|
} else if (CommonConstants.TableName.MED_MEDICATION_REQUEST.equals(item.getRequestTable())) {
|
||||||
|
medRequestList.add(item);
|
||||||
|
} else if (CommonConstants.TableName.WOR_DEVICE_REQUEST.equals(item.getRequestTable())) {
|
||||||
|
deviceRequestList.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> allRequestIds = performInfoList.stream().map(PerformInfoDto::getRequestId).toList();
|
||||||
|
|
||||||
|
// 校验①:校验医嘱是否已执行。若医嘱状态已经是“已执行”,则不允许撤销校对。
|
||||||
|
List<Procedure> allProcedures = procedureService.list(
|
||||||
|
new LambdaQueryWrapper<Procedure>()
|
||||||
|
.in(Procedure::getRequestId, allRequestIds)
|
||||||
|
.eq(Procedure::getDeleteFlag, "0"));
|
||||||
|
Set<Long> executedIds = allProcedures.stream()
|
||||||
|
.filter(p -> EventStatus.COMPLETED.getValue().equals(p.getStatusEnum()))
|
||||||
|
.map(Procedure::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Set<Long> cancelledRefundIds = allProcedures.stream()
|
||||||
|
.filter(p -> EventStatus.CANCEL.getValue().equals(p.getStatusEnum()) && p.getRefundId() != null)
|
||||||
|
.map(Procedure::getRefundId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
executedIds.removeAll(cancelledRefundIds);
|
||||||
|
if (!executedIds.isEmpty()) {
|
||||||
|
return R.fail("该医嘱已执行,无法撤销校对,请先去医嘱执行模块取消执行");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验②:校验该医嘱是否已记账扣费。若已扣费,则不允许撤销校对。
|
||||||
|
List<ChargeItem> chargeItems = chargeItemService.getChargeItemInfoByReqId(allRequestIds);
|
||||||
|
boolean isBilled = chargeItems.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum()));
|
||||||
|
if (isBilled) {
|
||||||
|
return R.fail("该医嘱已记账收费,若需撤销请先进行退费/计账回滚");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验③:若为药品医嘱,校验药房是否已发药(配药)。若已发药,则不允许撤销校对。
|
||||||
|
if (!medRequestList.isEmpty()) {
|
||||||
|
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
|
||||||
|
List<MedicationDispense> dispenseList = medicationDispenseService.list(
|
||||||
|
new LambdaQueryWrapper<MedicationDispense>()
|
||||||
|
.in(MedicationDispense::getMedReqId, medReqIds)
|
||||||
|
.in(MedicationDispense::getStatusEnum, Arrays.asList(
|
||||||
|
DispenseStatus.COMPLETED.getValue(),
|
||||||
|
DispenseStatus.PREPARED.getValue(),
|
||||||
|
DispenseStatus.PART_COMPLETED.getValue()
|
||||||
|
)));
|
||||||
|
if (!dispenseList.isEmpty()) {
|
||||||
|
return R.fail("药房已发药,请先进行退药申请");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 满足所有校验,执行撤销校对(回退至“未校对”,即 ACTIVE 状态)
|
||||||
|
if (!serviceRequestList.isEmpty()) {
|
||||||
|
serviceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>()
|
||||||
|
.in(ServiceRequest::getId, serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList())
|
||||||
|
.set(ServiceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
|
||||||
|
.set(ServiceRequest::getPerformerCheckId, null)
|
||||||
|
.set(ServiceRequest::getCheckTime, null));
|
||||||
|
}
|
||||||
|
if (!medRequestList.isEmpty()) {
|
||||||
|
medicationRequestService.update(new LambdaUpdateWrapper<MedicationRequest>()
|
||||||
|
.in(MedicationRequest::getId, medRequestList.stream().map(PerformInfoDto::getRequestId).toList())
|
||||||
|
.set(MedicationRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
|
||||||
|
.set(MedicationRequest::getPerformerCheckId, null)
|
||||||
|
.set(MedicationRequest::getCheckTime, null));
|
||||||
|
}
|
||||||
|
if (!deviceRequestList.isEmpty()) {
|
||||||
|
deviceRequestService.update(new LambdaUpdateWrapper<DeviceRequest>()
|
||||||
|
.in(DeviceRequest::getId, deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList())
|
||||||
|
.set(DeviceRequest::getStatusEnum, RequestStatus.ACTIVE.getValue())
|
||||||
|
.set(DeviceRequest::getPerformerCheckId, null)
|
||||||
|
.set(DeviceRequest::getCheckTime, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok(null, "撤销校对成功");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 医嘱执行
|
* 医嘱执行
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
|
|||||||
.map(notPerformedReason -> new DispenseInitDto.NotPerformedReasonOption(notPerformedReason.getValue(),
|
.map(notPerformedReason -> new DispenseInitDto.NotPerformedReasonOption(notPerformedReason.getValue(),
|
||||||
notPerformedReason.getInfo()))
|
notPerformedReason.getInfo()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
// 发药状态(汇总单:待配药→已提交,已发放→已发药)
|
// 发药状态(汇总单:汇总申请→待发药,发药→已发药)
|
||||||
List<DispenseStatusOption> dispenseStatusOptions = new ArrayList<>();
|
List<DispenseStatusOption> dispenseStatusOptions = new ArrayList<>();
|
||||||
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.PREPARATION.getValue(), "已提交"));
|
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.PREPARATION.getValue(), "待发药"));
|
||||||
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.COMPLETED.getValue(), "已发药"));
|
dispenseStatusOptions.add(new DispenseStatusOption(DispenseStatus.COMPLETED.getValue(), "已发药"));
|
||||||
|
|
||||||
initDto.setNotPerformedReasonOptions(notPerformedReasonOptions).setDispenseStatusOptions(dispenseStatusOptions);
|
initDto.setNotPerformedReasonOptions(notPerformedReasonOptions).setDispenseStatusOptions(dispenseStatusOptions);
|
||||||
@@ -309,11 +309,11 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 汇总发药单状态展示文案(药品医嘱状态映射表:汇总申请→已提交,发药→已发药)
|
* 汇总发药单状态展示文案(药品医嘱状态映射表:汇总申请→待发药,发药→已发药)
|
||||||
*/
|
*/
|
||||||
private String getSummaryFormStatusText(Integer statusEnum) {
|
private String getSummaryFormStatusText(Integer statusEnum) {
|
||||||
if (DispenseStatus.EXECUTED.getValue().equals(statusEnum)) {
|
if (DispenseStatus.PREPARATION.getValue().equals(statusEnum)) {
|
||||||
return "已提交";
|
return "待发药";
|
||||||
}
|
}
|
||||||
if (DispenseStatus.COMPLETED.getValue().equals(statusEnum)) {
|
if (DispenseStatus.COMPLETED.getValue().equals(statusEnum)) {
|
||||||
return "已发药";
|
return "已发药";
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入出转管理 controller
|
* 入出转管理 controller
|
||||||
@@ -166,4 +168,40 @@ public class ATDManageController {
|
|||||||
public R<?> getPendingMedication(Long encounterId) {
|
public R<?> getPendingMedication(Long encounterId) {
|
||||||
return atdManageAppService.getPendingMedication(encounterId);
|
return atdManageAppService.getPendingMedication(encounterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退床 (取消分床)
|
||||||
|
*
|
||||||
|
* @param encounterId 住院患者id
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@PutMapping(value = "/cancel-bed-assignment")
|
||||||
|
public R<?> cancelBedAssignment(Long encounterId) {
|
||||||
|
return atdManageAppService.cancelBedAssignment(encounterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取转科筛选选项(转入病区、转入科室)
|
||||||
|
*
|
||||||
|
* @return 转科筛选选项
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/transfer-options")
|
||||||
|
public R<?> getTransferOptions() {
|
||||||
|
return atdManageAppService.getTransferOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换床
|
||||||
|
*
|
||||||
|
* @param encounterId 住院患者id
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@PutMapping(value = "/change-bed-assignment")
|
||||||
|
public R<?> changeBedAssignment(Long encounterId){
|
||||||
|
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
|
||||||
|
String targetBedIdStr = request.getParameter("targetBedId");
|
||||||
|
Long targetBedId = (targetBedIdStr == null || targetBedIdStr.trim().isEmpty()) ? null : Long.valueOf(targetBedIdStr);
|
||||||
|
return atdManageAppService.changeBedAssginment(encounterId, targetBedId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ public class AdviceProcessController {
|
|||||||
return adviceProcessAppService.adviceReject(performInfoList);
|
return adviceProcessAppService.adviceReject(performInfoList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销医嘱校对
|
||||||
|
*
|
||||||
|
* @param performInfoList 医嘱信息集合
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PutMapping(value = "/advice-cancel-verify")
|
||||||
|
public R<?> adviceCancelVerify(@RequestBody List<PerformInfoDto> performInfoList) {
|
||||||
|
return adviceProcessAppService.adviceCancelVerify(performInfoList);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 医嘱执行
|
* 医嘱执行
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ public class AdmissionPageParam {
|
|||||||
/** 入院病房 */
|
/** 入院病房 */
|
||||||
private Long houseId;
|
private Long houseId;
|
||||||
|
|
||||||
|
/** 转科目标病区(待转科患者筛选) */
|
||||||
|
private Long transferTargetWardId;
|
||||||
|
|
||||||
|
/** 转科目标科室(待转科患者筛选) */
|
||||||
|
private Long transferTargetOrgId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入院类型
|
* 入院类型
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -146,4 +146,18 @@ public class AdmissionPatientPageDto {
|
|||||||
*/
|
*/
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long patientId;
|
private Long patientId;
|
||||||
|
|
||||||
|
/** 转科目标病区ID(转入病区) */
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long targetWardId;
|
||||||
|
|
||||||
|
/** 转科目标病区名称 */
|
||||||
|
private String targetWardName;
|
||||||
|
|
||||||
|
/** 转科目标科室ID(转入科室) */
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long targetOrgId;
|
||||||
|
|
||||||
|
/** 转科目标科室名称 */
|
||||||
|
private String targetOrgName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.healthlink.his.web.inhospitalnursestation.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转科筛选选项 DTO
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @date 2025-06-25
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TransferOptionsDto {
|
||||||
|
|
||||||
|
/** 入院病区选项(转入病区) */
|
||||||
|
private List<OptionItem> wardListOptions = new ArrayList<>();
|
||||||
|
|
||||||
|
/** 入院病房选项(转入科室) */
|
||||||
|
private List<OptionItem> departmentListOptions = new ArrayList<>();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class OptionItem {
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ public class NursingRecordController {
|
|||||||
* @return 患者信息
|
* @return 患者信息
|
||||||
*/
|
*/
|
||||||
@GetMapping("/patient-page")
|
@GetMapping("/patient-page")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:list')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:list')")
|
||||||
public R<?> getPatientInfoPage(NursingSearchParam nursingSearchParam,
|
public R<?> getPatientInfoPage(NursingSearchParam nursingSearchParam,
|
||||||
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
|
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
|
||||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
@@ -60,7 +60,7 @@ public class NursingRecordController {
|
|||||||
* @return 患者护理记录单信息
|
* @return 患者护理记录单信息
|
||||||
*/
|
*/
|
||||||
@GetMapping("/nursing-patient-page")
|
@GetMapping("/nursing-patient-page")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:list')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:list')")
|
||||||
public R<?> getNursingPatientPage(NursingSearchParam nursingSearchParam,
|
public R<?> getNursingPatientPage(NursingSearchParam nursingSearchParam,
|
||||||
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
|
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
|
||||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
@@ -75,7 +75,7 @@ public class NursingRecordController {
|
|||||||
* @param nursingRecordDto 护理记录实体
|
* @param nursingRecordDto 护理记录实体
|
||||||
*/
|
*/
|
||||||
@PostMapping("/save-nursing")
|
@PostMapping("/save-nursing")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:add')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:add')")
|
||||||
public R<?> saveRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) {
|
public R<?> saveRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) {
|
||||||
return nursingRecordAppService.saveRecord(nursingRecordDto);
|
return nursingRecordAppService.saveRecord(nursingRecordDto);
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ public class NursingRecordController {
|
|||||||
* @param nursingRecordDto 护理记录实体
|
* @param nursingRecordDto 护理记录实体
|
||||||
*/
|
*/
|
||||||
@PostMapping("/update-nursing")
|
@PostMapping("/update-nursing")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:edit')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:edit')")
|
||||||
public R<?> updateRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) {
|
public R<?> updateRecord(@Validated @RequestBody NursingRecordDto nursingRecordDto) {
|
||||||
return nursingRecordAppService.updateRecord(nursingRecordDto);
|
return nursingRecordAppService.updateRecord(nursingRecordDto);
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ public class NursingRecordController {
|
|||||||
* @param recordList 记录单List
|
* @param recordList 记录单List
|
||||||
*/
|
*/
|
||||||
@PostMapping("/delete-nursing")
|
@PostMapping("/delete-nursing")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:remove')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:remove')")
|
||||||
public R<?> delRecord(@Validated @RequestBody List<NursingRecordDto> recordList) {
|
public R<?> delRecord(@Validated @RequestBody List<NursingRecordDto> recordList) {
|
||||||
return nursingRecordAppService.delRecord(recordList);
|
return nursingRecordAppService.delRecord(recordList);
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ public class NursingRecordController {
|
|||||||
* @return 患者护理记录单信息
|
* @return 患者护理记录单信息
|
||||||
*/
|
*/
|
||||||
@GetMapping("/emr-template-page")
|
@GetMapping("/emr-template-page")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:list')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:list')")
|
||||||
public R<?> getEmrTemplate(NursingSearchParam nursingSearchParam,
|
public R<?> getEmrTemplate(NursingSearchParam nursingSearchParam,
|
||||||
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
|
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
|
||||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
@@ -127,7 +127,7 @@ public class NursingRecordController {
|
|||||||
* @param emrTemplateDto 病历模板信息
|
* @param emrTemplateDto 病历模板信息
|
||||||
*/
|
*/
|
||||||
@PostMapping("/emr-template-save")
|
@PostMapping("/emr-template-save")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:add')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:add')")
|
||||||
public R<?> saveEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) {
|
public R<?> saveEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) {
|
||||||
return nursingRecordAppService.saveEmrTemplate(emrTemplateDto);
|
return nursingRecordAppService.saveEmrTemplate(emrTemplateDto);
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ public class NursingRecordController {
|
|||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/emr-template-del")
|
@PostMapping("/emr-template-del")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:remove')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:remove')")
|
||||||
public R<?> deleteEmrTemplate(@Validated @RequestBody List<Long> idList) {
|
public R<?> deleteEmrTemplate(@Validated @RequestBody List<Long> idList) {
|
||||||
return nursingRecordAppService.deleteEmrTemplate(idList);
|
return nursingRecordAppService.deleteEmrTemplate(idList);
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ public class NursingRecordController {
|
|||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/emr-template-update")
|
@PostMapping("/emr-template-update")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:edit')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:edit')")
|
||||||
public R<?> updateEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) {
|
public R<?> updateEmrTemplate(@Validated @RequestBody NursingEmrTemplateDto emrTemplateDto) {
|
||||||
return nursingRecordAppService.updateEmrTemplate(emrTemplateDto);
|
return nursingRecordAppService.updateEmrTemplate(emrTemplateDto);
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ public class NursingRecordController {
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/batch-save")
|
@PostMapping("/batch-save")
|
||||||
@PreAuthorize("hasAuthority('nursing:record:edit')")
|
@PreAuthorize("@ss.hasPermi('nursing:record:edit')")
|
||||||
public R<?> batchSaveRecord(@Validated @RequestBody BatchNursingRecordDto batchDto) {
|
public R<?> batchSaveRecord(@Validated @RequestBody BatchNursingRecordDto batchDto) {
|
||||||
return nursingRecordAppService.batchSaveRecord(batchDto);
|
return nursingRecordAppService.batchSaveRecord(batchDto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
|||||||
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
|
List<RegAdviceSaveDto> activityList = regAdviceSaveList.stream()
|
||||||
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
.filter(e -> ItemType.ACTIVITY.getValue().equals(e.getAdviceType())
|
||||||
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())
|
|| ItemType.SURGERY.getValue().equals(e.getAdviceType())
|
||||||
|
|| ItemType.TEXT.getValue().equals(e.getAdviceType())
|
||||||
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
|| (e.getAdviceType() != null && e.getAdviceType() == 26))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
// 耗材 🔧 Bug #147 修复
|
// 耗材 🔧 Bug #147 修复
|
||||||
@@ -687,7 +688,12 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
|||||||
longServiceRequest.setRateCode(regAdviceSaveDto.getRateCode()); // 用药频次
|
longServiceRequest.setRateCode(regAdviceSaveDto.getRateCode()); // 用药频次
|
||||||
longServiceRequest.setCategoryEnum(regAdviceSaveDto.getCategoryEnum()); // 请求类型
|
longServiceRequest.setCategoryEnum(regAdviceSaveDto.getCategoryEnum()); // 请求类型
|
||||||
longServiceRequest.setTherapyEnum(regAdviceSaveDto.getTherapyEnum()); // 治疗类型,长期(需要前端传)
|
longServiceRequest.setTherapyEnum(regAdviceSaveDto.getTherapyEnum()); // 治疗类型,长期(需要前端传)
|
||||||
longServiceRequest.setActivityId(regAdviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
|
// 文字医嘱(type=8)不走定价体系,activityId设置为0L占位
|
||||||
|
if (ItemType.TEXT.getValue().equals(regAdviceSaveDto.getAdviceType())) {
|
||||||
|
longServiceRequest.setActivityId(0L);
|
||||||
|
} else {
|
||||||
|
longServiceRequest.setActivityId(regAdviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
|
||||||
|
}
|
||||||
longServiceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
|
longServiceRequest.setPatientId(regAdviceSaveDto.getPatientId()); // 患者
|
||||||
longServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
|
longServiceRequest.setRequesterId(regAdviceSaveDto.getPractitionerId()); // 开方医生
|
||||||
longServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
|
longServiceRequest.setEncounterId(regAdviceSaveDto.getEncounterId()); // 就诊id
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class SurgerySafetyCheckController {
|
|||||||
return R.ok(safetyCheckService.list(w));
|
return R.ok(safetyCheckService.list(w));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id:\\d+}")
|
||||||
@Operation(summary = "获取安全核查详情")
|
@Operation(summary = "获取安全核查详情")
|
||||||
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
|
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
|
||||||
public R<?> getById(@PathVariable Long id) {
|
public R<?> getById(@PathVariable Long id) {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.healthlink.his.web.ybmock.controller;
|
||||||
|
|
||||||
|
import com.healthlink.his.yb.mock.domain.YbPsnInfo;
|
||||||
|
import com.healthlink.his.web.ybmock.service.YbMockService;
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医保模拟接口 Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/yb-mock")
|
||||||
|
@Slf4j
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Tag(name = "医保模拟接口")
|
||||||
|
public class YbMockController {
|
||||||
|
|
||||||
|
private final YbMockService ybMockService;
|
||||||
|
|
||||||
|
@PostMapping("/{apiCode}")
|
||||||
|
@Operation(summary = "医保模拟接口")
|
||||||
|
public R<?> handleApi(@PathVariable String apiCode, @RequestBody Map<String, String> params) {
|
||||||
|
log.info("收到医保请求: apiCode={}, params={}", apiCode, params);
|
||||||
|
try {
|
||||||
|
Map<String, Object> result;
|
||||||
|
switch (apiCode) {
|
||||||
|
case "1101":
|
||||||
|
result = ybMockService.getPatientInfo(params);
|
||||||
|
break;
|
||||||
|
case "2201":
|
||||||
|
result = ybMockService.clinicRegister(params);
|
||||||
|
break;
|
||||||
|
case "2203":
|
||||||
|
result = ybMockService.clinicPrescription(params);
|
||||||
|
break;
|
||||||
|
case "2207":
|
||||||
|
result = ybMockService.clinicSettle(params);
|
||||||
|
break;
|
||||||
|
case "3201":
|
||||||
|
result = ybMockService.inpatientRegister(params);
|
||||||
|
break;
|
||||||
|
case "3203":
|
||||||
|
result = ybMockService.inpatientPrescription(params);
|
||||||
|
break;
|
||||||
|
case "3207":
|
||||||
|
result = ybMockService.inpatientSettle(params);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = new HashMap<>();
|
||||||
|
result.put("infcode", -1);
|
||||||
|
result.put("err_msg", "未支持的接口: " + apiCode);
|
||||||
|
}
|
||||||
|
return R.ok(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("医保接口调用失败", e);
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("infcode", -1);
|
||||||
|
error.put("err_msg", "系统错误: " + e.getMessage());
|
||||||
|
return R.ok(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/psn/{psnNo}")
|
||||||
|
@Operation(summary = "获取参保人信息")
|
||||||
|
public R<?> getPatientInfo(@PathVariable String psnNo) {
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
params.put("psn_no", psnNo);
|
||||||
|
return R.ok(ybMockService.getPatientInfo(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/psn")
|
||||||
|
@Operation(summary = "添加参保人信息")
|
||||||
|
public R<?> addPsnInfo(@RequestBody YbPsnInfo psnInfo) {
|
||||||
|
return R.ok(ybMockService.addPsnInfo(psnInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/psn/list")
|
||||||
|
@Operation(summary = "获取参保人列表")
|
||||||
|
public R<?> listPsnInfo() {
|
||||||
|
return R.ok(ybMockService.listPsnInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package com.healthlink.his.web.ybmock.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.healthlink.his.yb.mock.domain.YbPsnInfo;
|
||||||
|
import com.healthlink.his.yb.mock.mapper.YbPsnInfoMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医保模拟服务
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class YbMockService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private YbPsnInfoMapper psnInfoMapper;
|
||||||
|
|
||||||
|
public Map<String, Object> getPatientInfo(Map<String, String> params) {
|
||||||
|
String psnNo = params.get("psn_no");
|
||||||
|
log.info("获取参保人信息: psnNo={}", psnNo);
|
||||||
|
|
||||||
|
YbPsnInfo psnInfo = psnInfoMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<YbPsnInfo>().eq(YbPsnInfo::getPsnNo, psnNo)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (psnInfo == null) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("infcode", -1);
|
||||||
|
error.put("err_msg", "未找到参保人信息: " + psnNo);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("infcode", 0);
|
||||||
|
result.put("output", buildPsnInfoOutput(psnInfo));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> clinicRegister(Map<String, String> params) {
|
||||||
|
log.info("门诊登记: params={}", params);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("infcode", 0);
|
||||||
|
Map<String, Object> output = new HashMap<>();
|
||||||
|
output.put("encounter_no", "MZ" + System.currentTimeMillis());
|
||||||
|
output.put("register_time", new Date().toString());
|
||||||
|
output.put("status", "成功");
|
||||||
|
result.put("output", output);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> clinicPrescription(Map<String, String> params) {
|
||||||
|
log.info("门诊处方上传: params={}", params);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("infcode", 0);
|
||||||
|
Map<String, Object> output = new HashMap<>();
|
||||||
|
output.put("recipe_no", "CF" + System.currentTimeMillis());
|
||||||
|
output.put("upload_time", new Date().toString());
|
||||||
|
output.put("total_amount", "156.80");
|
||||||
|
output.put("self_pay", "23.52");
|
||||||
|
output.put("insurance_pay", "133.28");
|
||||||
|
output.put("status", "成功");
|
||||||
|
result.put("output", output);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> clinicSettle(Map<String, String> params) {
|
||||||
|
log.info("门诊结算: params={}", params);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("infcode", 0);
|
||||||
|
Map<String, Object> output = new HashMap<>();
|
||||||
|
output.put("settle_no", "JZ" + System.currentTimeMillis());
|
||||||
|
output.put("total_amount", "156.80");
|
||||||
|
output.put("insurance_pay", "133.28");
|
||||||
|
output.put("self_pay", "23.52");
|
||||||
|
output.put("account_pay", "20.00");
|
||||||
|
output.put("cash_pay", "3.52");
|
||||||
|
output.put("settle_time", new Date().toString());
|
||||||
|
output.put("status", "成功");
|
||||||
|
result.put("output", output);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> inpatientRegister(Map<String, String> params) {
|
||||||
|
log.info("住院登记: params={}", params);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("infcode", 0);
|
||||||
|
Map<String, Object> output = new HashMap<>();
|
||||||
|
output.put("admission_no", "ZY" + System.currentTimeMillis());
|
||||||
|
output.put("admission_time", new Date().toString());
|
||||||
|
output.put("bed_no", "3-201-1");
|
||||||
|
output.put("status", "成功");
|
||||||
|
result.put("output", output);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> inpatientPrescription(Map<String, String> params) {
|
||||||
|
log.info("住院处方上传: params={}", params);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("infcode", 0);
|
||||||
|
Map<String, Object> output = new HashMap<>();
|
||||||
|
output.put("recipe_no", "ZYCF" + System.currentTimeMillis());
|
||||||
|
output.put("upload_time", new Date().toString());
|
||||||
|
output.put("total_amount", "2580.50");
|
||||||
|
output.put("insurance_pay", "2322.45");
|
||||||
|
output.put("self_pay", "258.05");
|
||||||
|
output.put("status", "成功");
|
||||||
|
result.put("output", output);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> inpatientSettle(Map<String, String> params) {
|
||||||
|
log.info("住院结算: params={}", params);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("infcode", 0);
|
||||||
|
Map<String, Object> output = new HashMap<>();
|
||||||
|
output.put("settle_no", "ZYJS" + System.currentTimeMillis());
|
||||||
|
output.put("total_amount", "15680.50");
|
||||||
|
output.put("insurance_pay", "14112.45");
|
||||||
|
output.put("self_pay", "1568.05");
|
||||||
|
output.put("account_pay", "1200.00");
|
||||||
|
output.put("cash_pay", "368.05");
|
||||||
|
output.put("settle_time", new Date().toString());
|
||||||
|
output.put("status", "成功");
|
||||||
|
result.put("output", output);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildPsnInfoOutput(YbPsnInfo psnInfo) {
|
||||||
|
Map<String, Object> output = new HashMap<>();
|
||||||
|
output.put("psn_no", psnInfo.getPsnNo());
|
||||||
|
output.put("psn_name", psnInfo.getPsnName());
|
||||||
|
output.put("sex_code", psnInfo.getSexCode());
|
||||||
|
output.put("sex_name", psnInfo.getSexName());
|
||||||
|
output.put("birth_date", psnInfo.getBirthDate());
|
||||||
|
output.put("id_card", psnInfo.getIdCard());
|
||||||
|
output.put("insur_type", psnInfo.getInsurType());
|
||||||
|
output.put("insur_area", psnInfo.getInsurArea());
|
||||||
|
output.put("card_no", psnInfo.getCardNo());
|
||||||
|
output.put("balance", psnInfo.getBalance().toString());
|
||||||
|
output.put("status", psnInfo.getStatus());
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public YbPsnInfo addPsnInfo(YbPsnInfo psnInfo) {
|
||||||
|
psnInfo.setCreateTime(new Date());
|
||||||
|
psnInfo.setUpdateTime(new Date());
|
||||||
|
psnInfoMapper.insert(psnInfo);
|
||||||
|
return psnInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<YbPsnInfo> listPsnInfo() {
|
||||||
|
return psnInfoMapper.selectList(new LambdaQueryWrapper<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ spring:
|
|||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8
|
url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8
|
||||||
username: postgresql
|
username: postgresql
|
||||||
password: Jchl1528 # 请替换为实际的数据库密码
|
password: Jchl1528 # 请替换为实际的数据库密码
|
||||||
# 从库数据源
|
# 从库数据源
|
||||||
@@ -73,9 +73,9 @@ spring:
|
|||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
# 地址
|
# 地址
|
||||||
host: 47.116.196.11
|
host: 192.168.110.252
|
||||||
# 端口,默认为6379
|
# 端口,默认为6379
|
||||||
port: 26379
|
port: 6379
|
||||||
# 数据库索引
|
# 数据库索引
|
||||||
database: 1
|
database: 1
|
||||||
# 密码
|
# 密码
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
|
# Flyway 数据库迁移配置
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
baseline-on-migrate: true
|
||||||
|
baseline-version: 0
|
||||||
|
locations: classpath:db/migration
|
||||||
|
out-of-order: false
|
||||||
|
validate-on-migrate: true
|
||||||
datasource:
|
datasource:
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
driverClassName: org.postgresql.Driver
|
driverClassName: org.postgresql.Driver
|
||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=hisdev&characterEncoding=UTF-8&client_encoding=UTF-8
|
url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8
|
||||||
username: postgresql
|
username: postgresql
|
||||||
password: Jchl1528 # 请替换为实际的数据库密码
|
password: Jchl1528 # 请替换为实际的数据库密码
|
||||||
# 从库数据源
|
# 从库数据源
|
||||||
@@ -49,7 +57,7 @@ spring:
|
|||||||
allow:
|
allow:
|
||||||
url-pattern: /druid/*
|
url-pattern: /druid/*
|
||||||
# 控制台管理用户名和密码
|
# 控制台管理用户名和密码
|
||||||
login-username: openhis
|
login-username: healthlink-his
|
||||||
login-password: 123456
|
login-password: 123456
|
||||||
filter:
|
filter:
|
||||||
stat:
|
stat:
|
||||||
@@ -62,27 +70,28 @@ spring:
|
|||||||
config:
|
config:
|
||||||
multi-statement-allow: true
|
multi-statement-allow: true
|
||||||
# redis 配置
|
# redis 配置
|
||||||
redis:
|
data:
|
||||||
# 地址
|
redis:
|
||||||
host: 192.168.110.252
|
# 地址
|
||||||
# 端口,默认为6379
|
host: 192.168.110.252
|
||||||
port: 6379
|
# 端口,默认为6379
|
||||||
# 数据库索引
|
port: 6379
|
||||||
database: 1
|
# 数据库索引
|
||||||
# 密码
|
database: 1
|
||||||
password: Jchl1528
|
# 密码
|
||||||
# 连接超时时间
|
password: Jchl1528
|
||||||
timeout: 10s
|
# 连接超时时间
|
||||||
lettuce:
|
timeout: 10s
|
||||||
pool:
|
lettuce:
|
||||||
# 连接池中的最小空闲连接
|
pool:
|
||||||
min-idle: 0
|
# 连接池中的最小空闲连接
|
||||||
# 连接池中的最大空闲连接
|
min-idle: 0
|
||||||
max-idle: 8
|
# 连接池中的最大空闲连接
|
||||||
# 连接池的最大数据库连接数
|
max-idle: 8
|
||||||
max-active: 8
|
# 连接池的最大数据库连接数
|
||||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
max-active: 8
|
||||||
max-wait: -1ms
|
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||||
|
max-wait: -1ms
|
||||||
|
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
@@ -90,4 +99,5 @@ server:
|
|||||||
port: 18080
|
port: 18080
|
||||||
servlet:
|
servlet:
|
||||||
# 应用的访问路径
|
# 应用的访问路径
|
||||||
context-path: /openhis
|
context-path: /healthlink-his
|
||||||
|
|
||||||
|
|||||||
@@ -160,4 +160,4 @@ management:
|
|||||||
db:
|
db:
|
||||||
enabled: true
|
enabled: true
|
||||||
redis:
|
redis:
|
||||||
enabled: true
|
enabled: false
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- V100__sync_existing_emr_data.sql
|
||||||
|
-- 同步已有的门诊/住院病历到修订历史和搜索索引
|
||||||
|
|
||||||
|
-- 1. 清空假数据
|
||||||
|
TRUNCATE TABLE emr_revision CASCADE;
|
||||||
|
TRUNCATE TABLE emr_search_index CASCADE;
|
||||||
|
|
||||||
|
-- 2. 从doc_emr同步修订历史(为每条病历创建初始修订记录)
|
||||||
|
INSERT INTO emr_revision (
|
||||||
|
id, emr_id, encounter_id, revision_number,
|
||||||
|
operator_id, operator_name, operation_type,
|
||||||
|
diff_content, snapshot_content, create_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
nextval('emr_revision_id_seq'),
|
||||||
|
e.id,
|
||||||
|
e.encounter_id,
|
||||||
|
1,
|
||||||
|
COALESCE(e.record_id, 1),
|
||||||
|
'系统同步',
|
||||||
|
'CREATE',
|
||||||
|
'初始创建 - 从历史病历同步',
|
||||||
|
e.context_json,
|
||||||
|
COALESCE(e.create_time, NOW())
|
||||||
|
FROM doc_emr e
|
||||||
|
WHERE e.id IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM emr_revision r WHERE r.emr_id = e.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 从doc_emr同步搜索索引(简化版,不依赖可能不存在的表)
|
||||||
|
INSERT INTO emr_search_index (
|
||||||
|
id, emr_id, encounter_id, patient_id, patient_name,
|
||||||
|
emr_type, emr_title, diagnosis_text,
|
||||||
|
doctor_name, department_name, create_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
nextval('emr_search_index_id_seq'),
|
||||||
|
e.id,
|
||||||
|
e.encounter_id,
|
||||||
|
e.patient_id,
|
||||||
|
'患者' || COALESCE(e.patient_id::text, '未知'),
|
||||||
|
CASE
|
||||||
|
WHEN e.class_enum = 1 THEN 'OUTPATIENT'
|
||||||
|
WHEN e.class_enum = 2 THEN 'INPATIENT'
|
||||||
|
ELSE 'OTHER'
|
||||||
|
END,
|
||||||
|
'未命名病历',
|
||||||
|
'',
|
||||||
|
'医生' || COALESCE(e.record_id::text, '未知'),
|
||||||
|
'未知科室',
|
||||||
|
COALESCE(e.create_time, NOW())
|
||||||
|
FROM doc_emr e
|
||||||
|
WHERE e.id IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM emr_search_index si WHERE si.emr_id = e.id
|
||||||
|
);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
-- V101__add_emr_sync_menu_and_permissions.sql
|
||||||
|
-- 添加EMR数据同步菜单和医生权限
|
||||||
|
|
||||||
|
-- 1. 添加EMR数据同步菜单(在电子病历管理下)
|
||||||
|
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||||
|
VALUES (
|
||||||
|
'EMR数据同步',
|
||||||
|
(SELECT menu_id FROM sys_menu WHERE menu_name = '电子病历管理' LIMIT 1),
|
||||||
|
99,
|
||||||
|
'sync',
|
||||||
|
'emr/sync/index',
|
||||||
|
'C',
|
||||||
|
'0',
|
||||||
|
'0',
|
||||||
|
'emr:sync:list',
|
||||||
|
'upload',
|
||||||
|
'admin',
|
||||||
|
NOW(),
|
||||||
|
'admin',
|
||||||
|
NOW(),
|
||||||
|
'EMR数据同步 - 从病历表同步数据到修订历史和搜索索引'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 为医生角色添加EMR权限
|
||||||
|
-- 获取医生角色ID(假设角色名为'医生'或'doctor')
|
||||||
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
SELECT
|
||||||
|
r.role_id,
|
||||||
|
m.menu_id
|
||||||
|
FROM sys_role r
|
||||||
|
CROSS JOIN sys_menu m
|
||||||
|
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生')
|
||||||
|
AND m.perms IN (
|
||||||
|
'emr:list',
|
||||||
|
'emr:edit',
|
||||||
|
'emr:sync:list'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys_role_menu rm
|
||||||
|
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 更新EMR相关菜单的权限(将inpatient:emr改为emr)
|
||||||
|
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'inpatient:emr:list';
|
||||||
|
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'inpatient:emr:edit';
|
||||||
|
UPDATE sys_menu SET perms = 'emr:list' WHERE perms = 'infection:emr:list';
|
||||||
|
UPDATE sys_menu SET perms = 'emr:edit' WHERE perms = 'infection:emr:edit';
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
-- V102__grant_emr_menu_to_doctor.sql
|
||||||
|
-- 为医生角色授予电子病历管理相关菜单权限
|
||||||
|
|
||||||
|
-- 1. 获取医生角色ID并授予EMR菜单权限
|
||||||
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
SELECT
|
||||||
|
r.role_id,
|
||||||
|
m.menu_id
|
||||||
|
FROM sys_role r
|
||||||
|
CROSS JOIN sys_menu m
|
||||||
|
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '管理员', 'admin')
|
||||||
|
AND m.menu_id IN (
|
||||||
|
20201, -- 电子病历管理
|
||||||
|
20202, -- 病案归档
|
||||||
|
20203, -- 修订历史
|
||||||
|
20204, -- 病历时效
|
||||||
|
20205, -- 病历检索
|
||||||
|
20206, -- 进程记录
|
||||||
|
20207 -- 知识库
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys_role_menu rm
|
||||||
|
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 为医生角色授予EMR相关权限
|
||||||
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
SELECT
|
||||||
|
r.role_id,
|
||||||
|
m.menu_id
|
||||||
|
FROM sys_role r
|
||||||
|
CROSS JOIN sys_menu m
|
||||||
|
WHERE r.role_name IN ('医生', 'doctor', '门诊医生', '住院医生', '管理员', 'admin')
|
||||||
|
AND m.perms IN (
|
||||||
|
'emr:list',
|
||||||
|
'emr:edit',
|
||||||
|
'document:progressnote:list',
|
||||||
|
'document:progressnote:add',
|
||||||
|
'document:progressnote:edit'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys_role_menu rm
|
||||||
|
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- V103__add_patient_info_to_emr_search_index.sql
|
||||||
|
-- 为病历检索索引添加患者基本信息
|
||||||
|
|
||||||
|
-- 添加患者信息字段
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_gender VARCHAR(10);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_age VARCHAR(10);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_phone VARCHAR(20);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_id_card VARCHAR(20);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS encounter_no VARCHAR(50);
|
||||||
|
|
||||||
|
-- 添加索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_emr_search_patient_name ON emr_search_index(patient_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_emr_search_encounter_no ON emr_search_index(encounter_no);
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
-- V105__create_yb_mock_tables.sql
|
||||||
|
-- 创建医保模拟服务器所需的数据库表
|
||||||
|
|
||||||
|
-- 1. 参保人信息表
|
||||||
|
CREATE TABLE IF NOT EXISTS yb_psn_info (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
psn_no VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
psn_name VARCHAR(100),
|
||||||
|
sex_code VARCHAR(10),
|
||||||
|
sex_name VARCHAR(20),
|
||||||
|
birth_date VARCHAR(20),
|
||||||
|
id_card VARCHAR(20),
|
||||||
|
insur_type VARCHAR(100),
|
||||||
|
insur_area VARCHAR(100),
|
||||||
|
card_no VARCHAR(50),
|
||||||
|
balance DECIMAL(12,2) DEFAULT 0,
|
||||||
|
status VARCHAR(20) DEFAULT '正常',
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 电子处方表
|
||||||
|
CREATE TABLE IF NOT EXISTS yb_recipe (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recipe_no VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
psn_no VARCHAR(50),
|
||||||
|
encounter_no VARCHAR(50),
|
||||||
|
recipe_type VARCHAR(20),
|
||||||
|
total_amount DECIMAL(12,2),
|
||||||
|
self_pay DECIMAL(12,2),
|
||||||
|
insurance_pay DECIMAL(12,2),
|
||||||
|
status VARCHAR(20) DEFAULT '待结算',
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 事前事中商品库
|
||||||
|
CREATE TABLE IF NOT EXISTS yb_product (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
item_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
item_name VARCHAR(200),
|
||||||
|
item_type VARCHAR(50),
|
||||||
|
spec VARCHAR(100),
|
||||||
|
unit VARCHAR(20),
|
||||||
|
price DECIMAL(12,2),
|
||||||
|
manufacturer VARCHAR(200),
|
||||||
|
approval_no VARCHAR(100),
|
||||||
|
status VARCHAR(20) DEFAULT '正常',
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 4. 结算记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS yb_settle_record (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
settle_no VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
psn_no VARCHAR(50),
|
||||||
|
encounter_no VARCHAR(50),
|
||||||
|
settle_type VARCHAR(20),
|
||||||
|
total_amount DECIMAL(12,2),
|
||||||
|
insurance_pay DECIMAL(12,2),
|
||||||
|
self_pay DECIMAL(12,2),
|
||||||
|
account_pay DECIMAL(12,2),
|
||||||
|
cash_pay DECIMAL(12,2),
|
||||||
|
status VARCHAR(20) DEFAULT '成功',
|
||||||
|
settle_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 5. 签到签退记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS yb_sign_record (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
psn_no VARCHAR(50),
|
||||||
|
sign_type VARCHAR(20),
|
||||||
|
sign_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
terminal_no VARCHAR(50),
|
||||||
|
status VARCHAR(20) DEFAULT '成功'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 6. 插入测试数据 - 参保人信息
|
||||||
|
INSERT INTO yb_psn_info (psn_no, psn_name, sex_code, sex_name, birth_date, id_card, insur_type, insur_area, card_no, balance, status) VALUES
|
||||||
|
('P100001', '张三', '1', '男', '1980-01-15', '450123198001151234', '职工基本医疗保险', '南宁市', 'C2024000001', 12580.50, '正常'),
|
||||||
|
('P100002', '李四', '2', '女', '1985-03-20', '450123198503201234', '城乡居民基本医疗保险', '柳州市', 'C2024000002', 5620.00, '正常'),
|
||||||
|
('P100003', '王五', '1', '男', '1990-06-10', '450123199006101234', '职工基本医疗保险', '桂林市', 'C2024000003', 8950.25, '正常'),
|
||||||
|
('P100004', '赵六', '2', '女', '1975-12-25', '450123197512251234', '离休人员医疗保险', '梧州市', 'C2024000004', 25000.00, '正常'),
|
||||||
|
('P100005', '孙七', '1', '男', '1995-08-08', '450123199508081234', '城乡居民基本医疗保险', '北海市', 'C2024000005', 3200.75, '暂停');
|
||||||
|
|
||||||
|
-- 7. 插入测试数据 - 电子处方
|
||||||
|
INSERT INTO yb_recipe (recipe_no, psn_no, encounter_no, recipe_type, total_amount, self_pay, insurance_pay, status) VALUES
|
||||||
|
('CF20240601001', 'P100001', 'MZ20240601001', '西药处方', 156.80, 23.52, 133.28, '已结算'),
|
||||||
|
('CF20240601002', 'P100002', 'MZ20240601002', '中药处方', 238.50, 71.55, 166.95, '待结算'),
|
||||||
|
('CF20240601003', 'P100003', 'ZY20240601001', '住院处方', 2580.50, 258.05, 2322.45, '已结算');
|
||||||
|
|
||||||
|
-- 8. 插入测试数据 - 结算记录
|
||||||
|
INSERT INTO yb_settle_record (settle_no, psn_no, encounter_no, settle_type, total_amount, insurance_pay, self_pay, account_pay, cash_pay, status) VALUES
|
||||||
|
('JZ20240601001', 'P100001', 'MZ20240601001', '门诊结算', 156.80, 133.28, 23.52, 20.00, 3.52, '成功'),
|
||||||
|
('ZYJS20240601001', 'P100003', 'ZY20240601001', '住院结算', 15680.50, 14112.45, 1568.05, 1200.00, 368.05, '成功');
|
||||||
|
|
||||||
|
-- 9. 创建索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yb_psn_info_psn_no ON yb_psn_info(psn_no);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yb_recipe_psn_no ON yb_recipe(psn_no);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yb_recipe_encounter ON yb_recipe(encounter_no);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yb_settle_psn_no ON yb_settle_record(psn_no);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yb_settle_encounter ON yb_settle_record(encounter_no);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yb_sign_psn_no ON yb_sign_record(psn_no);
|
||||||
|
|
||||||
|
-- 10. 修复 clinical_pathway 表缺失列
|
||||||
|
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
|
||||||
|
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
|
||||||
|
ALTER TABLE clinical_pathway ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;
|
||||||
|
|
||||||
|
-- 11. 修复 clinical_pathway_execution 表缺失列
|
||||||
|
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_by VARCHAR(64) DEFAULT '';
|
||||||
|
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_by VARCHAR(64) DEFAULT '';
|
||||||
|
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_time TIMESTAMP;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- V106__add_missing_emr_search_index_columns.sql
|
||||||
|
-- 补充 emr_search_index 缺失的患者信息列(V103 未生效)
|
||||||
|
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_gender VARCHAR(10);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_age VARCHAR(10);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_phone VARCHAR(20);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_id_card VARCHAR(20);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS encounter_no VARCHAR(50);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- V104__add_patient_info_to_emr_search_index_hisdev.sql
|
||||||
|
-- 在 healthlink_his schema 上添加患者信息字段
|
||||||
|
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_gender VARCHAR(10);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_age VARCHAR(10);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_phone VARCHAR(20);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS patient_id_card VARCHAR(20);
|
||||||
|
ALTER TABLE emr_search_index ADD COLUMN IF NOT EXISTS encounter_no VARCHAR(50);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user