main.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // Command ac15sp 打印一个(或多个)存储过程的**参数列表 + 正文原样**。
  2. //
  3. // 为什么这是常备工具而不是一次性探针:
  4. // AC-15 阶段 2 已三次证明「照抄 SP」不可行 —— 注释会写错(`sp_user_expense_baddebt`
  5. // 的 COMMENT 写着"用户余额充值")、正文会写坏(`sp_user_expense_school_spend` 的 INSERT
  6. // 2 列配 3 值)、默认备注会缺失。行为只能从**语句正文**里读,所以需要一个随时能查的入口。
  7. //
  8. // 用法:
  9. //
  10. // go run ./cmd/ac15sp -sp sp_user_expense_baddebt
  11. // go run ./cmd/ac15sp -sp sp_a -sp sp_b -db legacy_oracle
  12. //
  13. // 只读边界(硬性):
  14. // - 只查 `information_schema`(`parameters` / `routines`),不执行任何 DDL/DML;
  15. // - 连接建立后先 `SET SESSION TRANSACTION READ ONLY`,服务端也会拒绝写;
  16. // - 库名必须在允许清单内(只允许本机 fixture 库)。
  17. //
  18. // 口令:只从环境变量 `AC15_DB_PASS` / `MYSQL_PWD` 或仓库内 `config/dev.json` 读取,
  19. // 不打印、不进日志、不落盘。
  20. //
  21. // 输出刻意**不做任何美化**:正文按数据库中存储的样子原样打印(含换行与缩进),
  22. // 便于直接 grep / diff;不要在这里做格式化或转义,那会让"看到的就是存的"这条不成立。
  23. package main
  24. import (
  25. "database/sql"
  26. "encoding/json"
  27. "flag"
  28. "fmt"
  29. "os"
  30. "regexp"
  31. "strings"
  32. "gorm.io/driver/mysql"
  33. "gorm.io/gorm"
  34. "gorm.io/gorm/logger"
  35. )
  36. // allowedDatabases 只允许本机 fixture 库(与 dualrun/fixture 的口径一致)。
  37. var allowedDatabases = map[string]bool{
  38. "finance_bk": true,
  39. "legacy_finance": true,
  40. "legacy_oracle": true,
  41. "yuansiwei_2026": true,
  42. "yuansiwei_i": true,
  43. "e2e_test": true,
  44. }
  45. var allowedHosts = map[string]bool{"127.0.0.1": true, "localhost": true, "::1": true}
  46. type multiFlag []string
  47. func (m *multiFlag) String() string { return strings.Join(*m, ",") }
  48. func (m *multiFlag) Set(v string) error { *m = append(*m, v); return nil }
  49. var (
  50. dsnPattern = regexp.MustCompile(`^([^:]+):([^@]+)@tcp\(([^:]+):(\d+)\)`)
  51. jsonPattern = regexp.MustCompile(`"dsn"\s*:\s*"([^"]+)"`)
  52. )
  53. func main() {
  54. var sps multiFlag
  55. db := flag.String("db", "legacy_oracle", "库名(必须在允许清单内)")
  56. cfgPath := flag.String("config", "config/dev.json", "凭据来源(环境变量优先)")
  57. flag.Var(&sps, "sp", "存储过程名,可重复:-sp a -sp b")
  58. flag.Parse()
  59. // 兼容位置参数写法:ac15sp sp_a sp_b
  60. if len(sps) == 0 {
  61. sps = append(sps, flag.Args()...)
  62. }
  63. if len(sps) == 0 {
  64. fmt.Fprintln(os.Stderr, "用法: go run ./cmd/ac15sp -sp <procName> [-db legacy_oracle]")
  65. os.Exit(2)
  66. }
  67. if !allowedDatabases[*db] {
  68. fmt.Fprintf(os.Stderr, "拒绝:库 %q 不在允许清单内\n", *db)
  69. os.Exit(2)
  70. }
  71. host, port, user, pass, err := loadCreds(*cfgPath)
  72. if err != nil {
  73. fmt.Fprintln(os.Stderr, "凭据加载失败:", err)
  74. os.Exit(2)
  75. }
  76. if !allowedHosts[host] {
  77. fmt.Fprintln(os.Stderr, "拒绝:host 不在允许清单内(只允许本机)")
  78. os.Exit(2)
  79. }
  80. if pass == "" {
  81. fmt.Fprintln(os.Stderr, "拒绝:口令为空(用 AC15_DB_PASS / MYSQL_PWD 或 config/dev.json)")
  82. os.Exit(2)
  83. }
  84. dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&timeout=5s",
  85. user, pass, host, port, *db)
  86. g, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
  87. if err != nil {
  88. fmt.Fprintln(os.Stderr, "连接失败:", err)
  89. os.Exit(1)
  90. }
  91. sqlDB, err := g.DB()
  92. if err != nil {
  93. fmt.Fprintln(os.Stderr, "取连接失败:", err)
  94. os.Exit(1)
  95. }
  96. defer sqlDB.Close()
  97. // 服务端级只读:即使本程序写错也写不进去
  98. if _, err := sqlDB.Exec("SET SESSION TRANSACTION READ ONLY"); err != nil {
  99. fmt.Fprintln(os.Stderr, "设置只读会话失败:", err)
  100. os.Exit(1)
  101. }
  102. exit := 0
  103. for _, sp := range sps {
  104. if err := dumpOne(sqlDB, *db, sp); err != nil {
  105. fmt.Fprintf(os.Stderr, "[%s] %v\n", sp, err)
  106. exit = 1
  107. }
  108. }
  109. os.Exit(exit)
  110. }
  111. // dumpOne 打印参数列表 + 正文原样。
  112. //
  113. // 两个查询都落在 information_schema(不 SHOW,不读 mysql.*):
  114. // - 参数:information_schema.parameters
  115. // - 正文:information_schema.routines.routine_definition(**原样输出,不 trim 不转义**)
  116. func dumpOne(db *sql.DB, schema, sp string) error {
  117. var routineType, securityType, definer, comment, body, created, altered sql.NullString
  118. var dataAccess, deterministic sql.NullString
  119. err := db.QueryRow(`SELECT routine_type, IFNULL(sql_data_access,''), IFNULL(is_deterministic,''),
  120. IFNULL(security_type,''), IFNULL(definer,''), IFNULL(routine_comment,''),
  121. IFNULL(routine_definition,''), IFNULL(created,''), IFNULL(last_altered,'')
  122. FROM information_schema.routines
  123. WHERE routine_schema = ? AND routine_name = ?`, schema, sp).
  124. Scan(&routineType, &dataAccess, &deterministic, &securityType, &definer, &comment, &body, &created, &altered)
  125. if err == sql.ErrNoRows {
  126. return fmt.Errorf("在 %s 下找不到 routine %q", schema, sp)
  127. }
  128. if err != nil {
  129. return err
  130. }
  131. rows, err := db.Query(`SELECT ordinal_position, IFNULL(parameter_mode,''), IFNULL(parameter_name,''), IFNULL(dtd_identifier,'')
  132. FROM information_schema.parameters
  133. WHERE specific_schema = ? AND specific_name = ? AND ordinal_position > 0
  134. ORDER BY ordinal_position`, schema, sp)
  135. if err != nil {
  136. return err
  137. }
  138. defer rows.Close()
  139. type param struct {
  140. pos int
  141. mode string
  142. name string
  143. typ string
  144. }
  145. var params []param
  146. for rows.Next() {
  147. var p param
  148. if err := rows.Scan(&p.pos, &p.mode, &p.name, &p.typ); err == nil {
  149. params = append(params, p)
  150. }
  151. }
  152. if err := rows.Err(); err != nil {
  153. return err
  154. }
  155. fmt.Printf("===== %s.%s =====\n", schema, sp)
  156. fmt.Printf("type=%s data_access=%s deterministic=%s security=%s definer=%s\n",
  157. routineType.String, dataAccess.String, deterministic.String, securityType.String, definer.String)
  158. fmt.Printf("created=%s last_altered=%s\n", created.String, altered.String)
  159. fmt.Printf("comment=%s\n", comment.String)
  160. fmt.Printf("-- 参数(%d 个)--\n", len(params))
  161. if len(params) == 0 {
  162. fmt.Println("(无参数)")
  163. }
  164. for _, p := range params {
  165. fmt.Printf(" %d. %s %s %s\n", p.pos, p.mode, p.name, p.typ)
  166. }
  167. fmt.Println("-- 正文(原样,未做任何格式化/转义)--")
  168. // 正文原样输出:不 TrimSpace、不替换换行 —— "看到的就是数据库里存的"
  169. fmt.Print(body.String)
  170. if !strings.HasSuffix(body.String, "\n") {
  171. fmt.Println()
  172. }
  173. fmt.Println()
  174. return nil
  175. }
  176. // loadCreds 环境变量优先,其次 config/dev.json 的 db.dsn。
  177. //
  178. // 只取 host/port/user/pass;返回的 pass 只用于拼 DSN,不打印。
  179. func loadCreds(path string) (host string, port int, user, pass string, err error) {
  180. host, port = "127.0.0.1", 3306
  181. if v := os.Getenv("AC15_DB_HOST"); v != "" {
  182. host = v
  183. }
  184. user = os.Getenv("AC15_DB_USER")
  185. pass = os.Getenv("AC15_DB_PASS")
  186. if pass == "" {
  187. pass = os.Getenv("MYSQL_PWD")
  188. }
  189. raw, rerr := os.ReadFile(path)
  190. if rerr != nil {
  191. if user == "" || pass == "" {
  192. return "", 0, "", "", fmt.Errorf("读 %s 失败且环境变量不完整: %w", path, rerr)
  193. }
  194. return host, port, user, pass, nil
  195. }
  196. var f struct {
  197. DB struct {
  198. DSN string `json:"dsn"`
  199. } `json:"db"`
  200. }
  201. if jerr := json.Unmarshal(raw, &f); jerr != nil {
  202. if user != "" && pass != "" {
  203. return host, port, user, pass, nil
  204. }
  205. return "", 0, "", "", fmt.Errorf("解析 %s 失败: %w", path, jerr)
  206. }
  207. m := dsnPattern.FindStringSubmatch(f.DB.DSN)
  208. if m == nil {
  209. if user != "" && pass != "" {
  210. return host, port, user, pass, nil
  211. }
  212. return "", 0, "", "", fmt.Errorf("%s 里的 dsn 无法解析", path)
  213. }
  214. if user == "" {
  215. user = m[1]
  216. }
  217. if pass == "" {
  218. pass = m[2]
  219. }
  220. if os.Getenv("AC15_DB_HOST") == "" {
  221. host = m[3]
  222. }
  223. if v := m[4]; v != "" {
  224. fmt.Sscanf(v, "%d", &port)
  225. }
  226. return host, port, user, pass, nil
  227. }