generate_seed_sql.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. # -*- coding: utf-8 -*-
  2. """
  3. 从《智能穴位识别数据模型》xlsx 生成种子 SQL,并写回 V1__init.sql 中
  4. 「-- >>> GENERATED_SEED_START」与「-- <<< GENERATED_SEED_END」之间的内容。
  5. 读取工作表:
  6. - 第 1 张:穴位数据模型(筛选「艾灸椅产品穴位」为「是」,且身体部位∈{背部,颈部},
  7. 部位分区∈{背部,骶区,颈后部,腰部};列名支持「身体部位」或「身体补位」)
  8. - 第 2 张:症状模型 -> symptom
  9. - 第 3 张:部位定位模型 -> body_region
  10. 并追加:admin_role、admin_user(默认 admin/admin 的 BCrypt)、admin_user_role、device_model。
  11. 需在环境中安装:pip install openpyxl
  12. """
  13. import glob
  14. import os
  15. try:
  16. import openpyxl
  17. except ImportError:
  18. raise SystemExit("pip install openpyxl")
  19. BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  20. os.chdir(BASE)
  21. V1_SQL = os.path.join(BASE, "src", "main", "resources", "db", "migration", "V1__init.sql")
  22. START_TAG = "-- >>> GENERATED_SEED_START"
  23. END_TAG = "-- <<< GENERATED_SEED_END"
  24. def esc(s):
  25. if s is None:
  26. return "NULL"
  27. s = str(s).replace("\\", "\\\\").replace("'", "''").replace("\r", " ").replace("\n", " ")
  28. return "'" + s + "'"
  29. def main():
  30. paths = glob.glob(os.path.join(BASE, "*数据模型*.xlsx"))
  31. if not paths:
  32. paths = glob.glob(os.path.join(BASE, "*.xlsx"))
  33. paths = [p for p in paths if "数据模型" in os.path.basename(p) or "模型" in os.path.basename(p)]
  34. if not paths:
  35. raise SystemExit("No data model xlsx found")
  36. path = paths[0]
  37. wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
  38. lines = []
  39. lines.append("SET FOREIGN_KEY_CHECKS = 0;")
  40. # Symptoms sheet 2
  41. ws_sym = wb[wb.sheetnames[1]]
  42. rows_sym = list(ws_sym.iter_rows(values_only=True))
  43. lines.append("")
  44. lines.append("-- 症状字典(Excel「症状模型」)")
  45. lines.append("INSERT IGNORE INTO symptom (code, name) VALUES ")
  46. vals = []
  47. for row in rows_sym[1:]:
  48. if not row or row[0] is None:
  49. continue
  50. code = esc(str(row[0]).strip())
  51. name = esc(str(row[1]).strip() if len(row) > 1 and row[1] is not None else "")
  52. vals.append("({0}, {1})".format(code, name))
  53. lines.append(",\n".join(vals) + ";")
  54. # Body regions sheet 3
  55. ws_br = wb[wb.sheetnames[2]]
  56. rows_br = list(ws_br.iter_rows(values_only=True))
  57. lines.append("")
  58. lines.append("-- 身体部位与分区(Excel「部位定位模型」)")
  59. lines.append("INSERT IGNORE INTO body_region (body_part, region_partition) VALUES ")
  60. vals = []
  61. for row in rows_br[1:]:
  62. if row is None:
  63. continue
  64. bp = row[0]
  65. rp = row[1]
  66. if bp is None and rp is None:
  67. continue
  68. vals.append("({0}, {1})".format(esc(bp), esc(rp)))
  69. lines.append(",\n".join(vals) + ";")
  70. # Acupoints sheet 1
  71. ws = wb[wb.sheetnames[0]]
  72. header = next(ws.iter_rows(min_row=1, max_row=1, values_only=True))
  73. h = list(header)
  74. def col(*names):
  75. for n in names:
  76. if n in h:
  77. return h.index(n)
  78. raise SystemExit("Excel 表头缺少列: " + " / ".join(names))
  79. idx_chair = col("艾灸椅产品穴位")
  80. idx_body = col("身体部位", "身体补位")
  81. cols = {
  82. "serial_no": h.index("序号"),
  83. "code": h.index("穴位编号"),
  84. "name": h.index("穴位名称"),
  85. "side_type": h.index("单/双穴"),
  86. "meridian": h.index("所属经络"),
  87. "body_region": idx_body,
  88. "region_partition": h.index("部位分区"),
  89. "location_text": h.index("穴位定位"),
  90. "relative_position_text": h.index("相对位置"),
  91. "indications": h.index("主管疾病"),
  92. "symptom_coverage": h.index("产品症状覆盖"),
  93. "acupoint_method": h.index("取穴"),
  94. "remark": len(h) - 1,
  95. }
  96. allowed_body = {"背部", "颈部"}
  97. allowed_partition = {"背部", "骶区", "颈后部", "腰部"}
  98. lines.append("")
  99. lines.append(
  100. "-- 标准穴位:艾灸椅=是 ∧ 身体部位∈(背部,颈部) ∧ 部位分区∈(背部,骶区,颈后部,腰部)"
  101. )
  102. lines.append(
  103. "INSERT IGNORE INTO acupoint (serial_no, code, name, side_type, meridian, body_region, region_partition, "
  104. "location_text, relative_position_text, indications, symptom_coverage, acupoint_method, chair_product, remark) VALUES "
  105. )
  106. vals = []
  107. for row in ws.iter_rows(min_row=2, values_only=True):
  108. if not row or row[idx_chair] != "是":
  109. continue
  110. r = list(row) + [None] * 20
  111. def g(key):
  112. i = cols[key]
  113. v = r[i] if i < len(r) else None
  114. return v
  115. br = g("body_region")
  116. rp = g("region_partition")
  117. if br is None or rp is None:
  118. continue
  119. brs = str(br).strip()
  120. rps = str(rp).strip()
  121. if brs not in allowed_body or rps not in allowed_partition:
  122. continue
  123. remark = r[cols["remark"]] if cols["remark"] < len(r) else None
  124. vals.append(
  125. "({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, 1, {12})".format(
  126. esc(g("serial_no")),
  127. esc(g("code")),
  128. esc(g("name")),
  129. esc(g("side_type")),
  130. esc(g("meridian")),
  131. esc(g("body_region")),
  132. esc(g("region_partition")),
  133. esc(g("location_text")),
  134. esc(g("relative_position_text")),
  135. esc(g("indications")),
  136. esc(g("symptom_coverage")),
  137. esc(g("acupoint_method")),
  138. esc(remark),
  139. )
  140. )
  141. lines.append(",\n".join(vals) + ";")
  142. bcrypt_password = "$2b$10$yW9CwM2u/vqz4CGZsQXPyeiKYtsu4PbAj.vKstKoY4hbFT6I0LiNm"
  143. lines.append("")
  144. lines.append("-- 后台角色、默认管理员、默认设备(联调)")
  145. lines.append(
  146. "INSERT IGNORE INTO admin_role (code, name) VALUES "
  147. "('SUPER_ADMIN', '超级管理员'), ('ADMIN', '管理员'), ('OPERATOR', '运营');"
  148. )
  149. lines.append(
  150. "INSERT IGNORE INTO admin_user (username, password_hash, display_name, status) VALUES "
  151. "('admin', '{0}', '系统管理员', 'ACTIVE');".format(bcrypt_password)
  152. )
  153. lines.append("INSERT IGNORE INTO admin_user_role (admin_user_id, admin_role_id) VALUES (1, 1);")
  154. lines.append(
  155. "INSERT IGNORE INTO device_model (code, name, stroke_range_x, stroke_range_y, stroke_range_z, "
  156. "origin_offset_x, origin_offset_y, origin_offset_z, safety_distance) VALUES "
  157. "('CHAIR-DEFAULT', '默认艾灸椅', 800, 600, 400, 0, 0, 0, 10);"
  158. )
  159. lines.append("-- INSERT IGNORE 不更新已存在的 admin 行:将管理员 admin 密码统一为明文 admin(BCrypt)")
  160. lines.append(
  161. "UPDATE admin_user SET password_hash = '{0}' WHERE username = 'admin' AND deleted = 0;".format(
  162. bcrypt_password
  163. )
  164. )
  165. lines.append("")
  166. lines.append("SET FOREIGN_KEY_CHECKS = 1;")
  167. with open(V1_SQL, encoding="utf-8") as f:
  168. content = f.read()
  169. clines = content.splitlines()
  170. si = ei = None
  171. for i, L in enumerate(clines):
  172. if L.strip() == START_TAG:
  173. si = i
  174. elif si is not None and L.strip() == END_TAG:
  175. ei = i
  176. break
  177. if si is None or ei is None or ei <= si:
  178. raise SystemExit("{0} 中缺少 {1} / {2} 标记".format(V1_SQL, START_TAG, END_TAG))
  179. new_clines = clines[: si + 1] + lines + clines[ei:]
  180. with open(V1_SQL, "w", encoding="utf-8", newline="\n") as f:
  181. f.write("\n".join(new_clines) + "\n")
  182. print("Updated seed in", V1_SQL, "acupoints:", len(vals))
  183. wb.close()
  184. if __name__ == "__main__":
  185. main()