payment_seed.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. package initialize
  2. import (
  3. "errors"
  4. "fmt"
  5. "gorm.io/gorm"
  6. "wails-app/internal/dao"
  7. "wails-app/internal/global"
  8. )
  9. type paymentMethodDefault struct {
  10. Code string
  11. Name string
  12. InputMode string
  13. Scenes []string
  14. Enabled bool
  15. Sort int
  16. Description string
  17. }
  18. type paymentEntryDefault struct {
  19. Code string
  20. Name string
  21. Enabled bool
  22. Sort int
  23. Description string
  24. Methods []string
  25. }
  26. // EnsurePaymentConfiguration upgrades both new and existing databases. The
  27. // method and entry tables are deliberately seeded independently: an entry
  28. // controls where payment starts, while a method controls how it settles.
  29. func EnsurePaymentConfiguration() error {
  30. db := global.GVA_DB
  31. if db == nil {
  32. return nil
  33. }
  34. if err := db.AutoMigrate(&dao.PaymentEntryConfig{}, &dao.PaymentEntryMethod{}); err != nil {
  35. return fmt.Errorf("初始化支付入口表失败: %w", err)
  36. }
  37. methods := []paymentMethodDefault{
  38. {Code: "cash", Name: "现金支付", InputMode: "manual_amount", Scenes: []string{"counter"}, Enabled: true, Sort: 10, Description: "以现金形式完成收款"},
  39. {Code: "pos", Name: "POS机支付", InputMode: "confirm", Scenes: []string{"counter"}, Enabled: false, Sort: 20, Description: "通过POS机完成支付后由岗亭人员确认"},
  40. {Code: "ticket_qr", Name: "小票扫码支付", InputMode: "external", Scenes: []string{"ticket"}, Enabled: false, Sort: 30, Description: "车主扫描停车小票发起外部支付"},
  41. {Code: "wechat", Name: "微信支付", InputMode: "external", Scenes: []string{"exit_kiosk"}, Enabled: true, Sort: 40, Description: "车主扫描订单二维码通过微信支付(出口自助)"},
  42. {Code: "alipay", Name: "支付宝", InputMode: "external", Scenes: []string{"exit_kiosk"}, Enabled: false, Sort: 50, Description: "车主扫描订单二维码通过支付宝支付(出口自助)"},
  43. }
  44. for _, item := range methods {
  45. method := dao.PaymentMethodConfig{}
  46. err := db.Where("code = ?", item.Code).First(&method).Error
  47. if errors.Is(err, gorm.ErrRecordNotFound) {
  48. method = dao.PaymentMethodConfig{
  49. Code: item.Code, Name: item.Name, InputMode: item.InputMode, Scenes: item.Scenes,
  50. Enabled: item.Enabled, Sort: item.Sort, IsSystem: true, Description: item.Description,
  51. }
  52. err = db.Create(&method).Error
  53. }
  54. if err != nil {
  55. return fmt.Errorf("初始化支付方式 %s 失败: %w", item.Code, err)
  56. }
  57. }
  58. // 现金支付是基础能力,升级时也要修复旧数据中被关闭或改型的情况。
  59. cashDefaults := dao.PaymentMethodConfig{
  60. Name: "现金支付", Description: "以现金形式完成收款",
  61. Enabled: true, InputMode: "manual_amount", Scenes: []string{"counter"}, IsSystem: true,
  62. }
  63. if err := db.Model(&dao.PaymentMethodConfig{}).Where("code = ?", "cash").
  64. Select("name", "description", "enabled", "input_mode", "scenes", "is_system").Updates(&cashDefaults).Error; err != nil {
  65. return err
  66. }
  67. entries := []paymentEntryDefault{
  68. {Code: "counter", Name: "收费岗亭", Enabled: true, Sort: 10, Description: "由收费岗亭人员发起收费和放行", Methods: []string{"cash", "pos"}},
  69. {Code: "ticket", Name: "停车小票", Enabled: true, Sort: 20, Description: "由停车小票或数字票发起后续支付", Methods: []string{"cash", "pos", "ticket_qr"}},
  70. {Code: "exit_kiosk", Name: "出口自助", Enabled: true, Sort: 30, Description: "出口自助终端扫码支付到账后自动放行", Methods: []string{"wechat", "alipay"}},
  71. }
  72. for _, item := range entries {
  73. entry := dao.PaymentEntryConfig{}
  74. err := db.Where("code = ?", item.Code).First(&entry).Error
  75. if errors.Is(err, gorm.ErrRecordNotFound) {
  76. entry = dao.PaymentEntryConfig{
  77. Code: item.Code, Name: item.Name, Enabled: item.Enabled, Sort: item.Sort,
  78. IsSystem: true, Description: item.Description,
  79. }
  80. if err = db.Create(&entry).Error; err != nil {
  81. return fmt.Errorf("初始化支付入口 %s 失败: %w", item.Code, err)
  82. }
  83. }
  84. if err != nil {
  85. return fmt.Errorf("读取支付入口 %s 失败: %w", item.Code, err)
  86. }
  87. var relationCount int64
  88. if err := db.Model(&dao.PaymentEntryMethod{}).Where("entry_code = ?", item.Code).Count(&relationCount).Error; err != nil {
  89. return err
  90. }
  91. if relationCount == 0 {
  92. for i, methodCode := range item.Methods {
  93. if err := db.Create(&dao.PaymentEntryMethod{EntryCode: item.Code, MethodCode: methodCode, Sort: i * 10}).Error; err != nil {
  94. return fmt.Errorf("初始化支付入口 %s 关联失败: %w", item.Code, err)
  95. }
  96. }
  97. }
  98. }
  99. // Migrate old method.scenes values into the relation table without
  100. // overwriting any explicit entry configuration made by an administrator.
  101. var legacyMethods []dao.PaymentMethodConfig
  102. if err := db.Where("is_legacy = ?", false).Find(&legacyMethods).Error; err != nil {
  103. return err
  104. }
  105. for _, method := range legacyMethods {
  106. for _, entryCode := range method.Scenes {
  107. var entry dao.PaymentEntryConfig
  108. if err := db.Where("code = ?", entryCode).First(&entry).Error; err != nil {
  109. continue
  110. }
  111. var count int64
  112. if err := db.Model(&dao.PaymentEntryMethod{}).Where("entry_code = ? AND method_code = ?", entryCode, method.Code).Count(&count).Error; err != nil {
  113. return err
  114. }
  115. if count == 0 {
  116. if err := db.Create(&dao.PaymentEntryMethod{EntryCode: entryCode, MethodCode: method.Code, Sort: method.Sort}).Error; err != nil {
  117. return err
  118. }
  119. }
  120. }
  121. }
  122. var parkingMenu dao.SysBaseMenu
  123. if err := db.Where("name = ?", "parking").First(&parkingMenu).Error; err == nil {
  124. methodMenu, err := ensurePaymentMenu(db, parkingMenu.ID, "paymentMethodConfig", "paymentMethod", "view/parking/paymentMethod.vue", "支付方式", "credit-card", 8)
  125. if err != nil {
  126. return err
  127. }
  128. entryMenu, err := ensurePaymentMenu(db, parkingMenu.ID, "paymentEntryConfig", "paymentEntry", "view/parking/paymentEntry.vue", "支付入口", "connection", 9)
  129. if err != nil {
  130. return err
  131. }
  132. ticketMenu, err := ensurePaymentMenu(db, parkingMenu.ID, "digitalTicket", "digitalTicket", "view/parking/digitalTicket.vue", "数字票", "tickets", 10)
  133. if err != nil {
  134. return err
  135. }
  136. for _, menu := range []*dao.SysBaseMenu{methodMenu, entryMenu} {
  137. for _, authorityID := range []string{"888", "9527"} {
  138. var count int64
  139. if err := db.Model(&dao.SysAuthorityMenu{}).
  140. Where("sys_base_menu_id = ? AND sys_authority_authority_id = ?", menu.ID, authorityID).Count(&count).Error; err != nil {
  141. return fmt.Errorf("初始化支付菜单权限失败: %w", err)
  142. }
  143. if count == 0 {
  144. link := dao.SysAuthorityMenu{MenuId: fmt.Sprint(menu.ID), AuthorityId: authorityID}
  145. if err := db.Create(&link).Error; err != nil {
  146. return fmt.Errorf("初始化支付菜单权限失败: %w", err)
  147. }
  148. }
  149. }
  150. }
  151. for _, authorityID := range []string{"618", "888", "9527"} {
  152. var count int64
  153. if err := db.Model(&dao.SysAuthorityMenu{}).
  154. Where("sys_base_menu_id = ? AND sys_authority_authority_id = ?", ticketMenu.ID, authorityID).Count(&count).Error; err != nil {
  155. return fmt.Errorf("初始化数字票菜单权限失败: %w", err)
  156. }
  157. if count == 0 {
  158. link := dao.SysAuthorityMenu{MenuId: fmt.Sprint(ticketMenu.ID), AuthorityId: authorityID}
  159. if err := db.Create(&link).Error; err != nil {
  160. return fmt.Errorf("初始化数字票菜单权限失败: %w", err)
  161. }
  162. }
  163. }
  164. }
  165. apis := []dao.SysApi{
  166. {Path: "/payment/methods", Description: "查询支付方式配置", ApiGroup: "支付方式", Method: "GET"},
  167. {Path: "/payment/methods/enabled", Description: "查询入口可用支付方式", ApiGroup: "支付方式", Method: "GET"},
  168. {Path: "/payment/methods/options", Description: "查询支付方式选项", ApiGroup: "支付方式", Method: "GET"},
  169. {Path: "/payment/methods", Description: "新增支付方式", ApiGroup: "支付方式", Method: "POST"},
  170. {Path: "/payment/methods/:id", Description: "更新支付方式", ApiGroup: "支付方式", Method: "PUT"},
  171. {Path: "/payment/methods/:id/status", Description: "启停支付方式", ApiGroup: "支付方式", Method: "PATCH"},
  172. {Path: "/payment/methods/:id", Description: "删除支付方式", ApiGroup: "支付方式", Method: "DELETE"},
  173. {Path: "/payment/entries", Description: "查询支付入口配置", ApiGroup: "支付入口", Method: "GET"},
  174. {Path: "/payment/entries/enabled", Description: "查询启用的支付入口", ApiGroup: "支付入口", Method: "GET"},
  175. {Path: "/payment/entries/options", Description: "查询支付入口选项", ApiGroup: "支付入口", Method: "GET"},
  176. {Path: "/payment/entries", Description: "新增支付入口", ApiGroup: "支付入口", Method: "POST"},
  177. {Path: "/payment/entries/:id", Description: "更新支付入口", ApiGroup: "支付入口", Method: "PUT"},
  178. {Path: "/payment/entries/:id/status", Description: "启停支付入口", ApiGroup: "支付入口", Method: "PATCH"},
  179. {Path: "/payment/entries/:id", Description: "删除支付入口", ApiGroup: "支付入口", Method: "DELETE"},
  180. {Path: "/digital-ticket/summary", Description: "查询数字票汇总", ApiGroup: "数字票", Method: "GET"},
  181. {Path: "/digital-ticket/list", Description: "查询数字票列表", ApiGroup: "数字票", Method: "GET"},
  182. {Path: "/digital-ticket/:ticketNo", Description: "查询数字票详情", ApiGroup: "数字票", Method: "GET"},
  183. {Path: "/digital-ticket/:ticketNo/pay", Description: "确认数字票支付", ApiGroup: "数字票", Method: "POST"},
  184. {Path: "/digital-ticket/:ticketNo/exit", Description: "确认数字票离场", ApiGroup: "数字票", Method: "POST"},
  185. }
  186. for _, api := range apis {
  187. var existing dao.SysApi
  188. err := db.Where("path = ? AND method = ?", api.Path, api.Method).First(&existing).Error
  189. if errors.Is(err, gorm.ErrRecordNotFound) {
  190. err = db.Create(&api).Error
  191. }
  192. if err != nil {
  193. return fmt.Errorf("初始化支付API失败: %w", err)
  194. }
  195. }
  196. if err := db.Exec(`CREATE TABLE IF NOT EXISTS casbin_rule (
  197. id INTEGER PRIMARY KEY AUTOINCREMENT,
  198. ptype TEXT, v0 TEXT, v1 TEXT, v2 TEXT, v3 TEXT, v4 TEXT, v5 TEXT
  199. )`).Error; err != nil {
  200. return err
  201. }
  202. readRules := [][2]string{
  203. {"/payment/methods/enabled", "GET"}, {"/payment/methods/options", "GET"},
  204. {"/payment/entries/enabled", "GET"}, {"/payment/entries/options", "GET"},
  205. }
  206. for _, role := range []string{"618", "888", "9527"} {
  207. for _, rule := range readRules {
  208. if err := ensureCasbinRule(role, rule[0], rule[1]); err != nil {
  209. return err
  210. }
  211. }
  212. }
  213. for _, role := range []string{"888", "9527"} {
  214. for _, rule := range [][2]string{
  215. {"/payment/methods", "GET"}, {"/payment/methods", "POST"},
  216. {"/payment/methods/:id", "PUT"}, {"/payment/methods/:id/status", "PATCH"},
  217. {"/payment/methods/:id", "DELETE"},
  218. {"/payment/entries", "GET"}, {"/payment/entries", "POST"},
  219. {"/payment/entries/:id", "PUT"}, {"/payment/entries/:id/status", "PATCH"},
  220. {"/payment/entries/:id", "DELETE"},
  221. } {
  222. if err := ensureCasbinRule(role, rule[0], rule[1]); err != nil {
  223. return err
  224. }
  225. }
  226. }
  227. for _, role := range []string{"618", "888", "9527"} {
  228. for _, rule := range [][2]string{
  229. {"/digital-ticket/summary", "GET"},
  230. {"/digital-ticket/list", "GET"},
  231. {"/digital-ticket/:ticketNo", "GET"},
  232. {"/digital-ticket/:ticketNo/pay", "POST"},
  233. {"/digital-ticket/:ticketNo/exit", "POST"},
  234. } {
  235. if err := ensureCasbinRule(role, rule[0], rule[1]); err != nil {
  236. return err
  237. }
  238. }
  239. }
  240. return nil
  241. }
  242. func ensurePaymentMenu(db *gorm.DB, parentID uint, name, path, component, title, icon string, sort int) (*dao.SysBaseMenu, error) {
  243. menu := &dao.SysBaseMenu{}
  244. err := db.Where("name = ?", name).First(menu).Error
  245. if errors.Is(err, gorm.ErrRecordNotFound) {
  246. *menu = dao.SysBaseMenu{
  247. MenuLevel: 0, ParentId: parentID, Path: path, Name: name,
  248. Hidden: false, Component: component, Sort: sort,
  249. Meta: dao.Meta{Title: title, Icon: icon},
  250. }
  251. if err := db.Create(menu).Error; err != nil {
  252. return nil, fmt.Errorf("初始化%s菜单失败: %w", title, err)
  253. }
  254. return menu, nil
  255. }
  256. if err != nil {
  257. return nil, fmt.Errorf("读取%s菜单失败: %w", title, err)
  258. }
  259. if err := db.Model(menu).Updates(map[string]interface{}{"parent_id": parentID, "path": path, "component": component, "sort": sort}).Error; err != nil {
  260. return nil, fmt.Errorf("更新%s菜单失败: %w", title, err)
  261. }
  262. return menu, nil
  263. }
  264. func ensureCasbinRule(role, path, method string) error {
  265. db := global.GVA_DB
  266. var count int64
  267. if err := db.Table("casbin_rule").Where("ptype = ? AND v0 = ? AND v1 = ? AND v2 = ?", "p", role, path, method).Count(&count).Error; err != nil {
  268. return err
  269. }
  270. if count == 0 {
  271. return db.Exec("INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES (?, ?, ?, ?, '', '', '')", "p", role, path, method).Error
  272. }
  273. return nil
  274. }