sysProductMemberModel_test.go 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185
  1. package productmember
  2. import (
  3. "context"
  4. "errors"
  5. "github.com/go-sql-driver/mysql"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. "github.com/zeromicro/go-zero/core/stores/sqlx"
  9. "math/rand"
  10. "perms-system-server/internal/consts"
  11. "perms-system-server/internal/testutil"
  12. "testing"
  13. "time"
  14. )
  15. func randProductMemberUserId() int64 {
  16. return int64(900000 + rand.Intn(100000))
  17. }
  18. // TC-0310: 正常插入
  19. func TestSysProductMemberModel_CRUD(t *testing.T) {
  20. ctx := context.Background()
  21. conn := testutil.GetTestSqlConn()
  22. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  23. pc := "t_pm_" + testutil.UniqueId()
  24. userId := randProductMemberUserId()
  25. ts := time.Now().Unix()
  26. data := &SysProductMember{
  27. ProductCode: pc,
  28. UserId: userId,
  29. MemberType: "MEMBER",
  30. Status: 1,
  31. CreateTime: ts,
  32. UpdateTime: ts,
  33. }
  34. res, err := m.Insert(ctx, data)
  35. if err != nil {
  36. t.Fatalf("Insert: %v", err)
  37. }
  38. id, err := res.LastInsertId()
  39. if err != nil {
  40. t.Fatalf("LastInsertId: %v", err)
  41. }
  42. defer testutil.CleanTable(ctx, conn, "sys_product_member", id)
  43. got, err := m.FindOne(ctx, id)
  44. if err != nil {
  45. t.Fatalf("FindOne: %v", err)
  46. }
  47. if got.ProductCode != pc || got.UserId != userId {
  48. t.Fatalf("FindOne mismatch: %+v", got)
  49. }
  50. byPair, err := m.FindOneByProductCodeUserId(ctx, pc, userId)
  51. if err != nil {
  52. t.Fatalf("FindOneByProductCodeUserId: %v", err)
  53. }
  54. if byPair.Id != id {
  55. t.Fatalf("FindOneByProductCodeUserId id want %d got %d", id, byPair.Id)
  56. }
  57. newTs := ts + 1
  58. got.Status = 2
  59. got.UpdateTime = newTs
  60. if err := m.Update(ctx, got); err != nil {
  61. t.Fatalf("Update: %v", err)
  62. }
  63. updated, err := m.FindOne(ctx, id)
  64. if err != nil {
  65. t.Fatalf("FindOne after update: %v", err)
  66. }
  67. if updated.Status != 2 || updated.UpdateTime != newTs {
  68. t.Fatalf("after Update: %+v", updated)
  69. }
  70. if err := m.Delete(ctx, id); err != nil {
  71. t.Fatalf("Delete: %v", err)
  72. }
  73. if _, err := m.FindOne(ctx, id); err != ErrNotFound {
  74. t.Fatalf("after Delete want ErrNotFound got %v", err)
  75. }
  76. }
  77. // TC-0475: 正常分页
  78. func TestSysProductMemberModel_FindListByProductCode(t *testing.T) {
  79. ctx := context.Background()
  80. conn := testutil.GetTestSqlConn()
  81. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  82. pc := "t_pm_page_" + testutil.UniqueId()
  83. ts := time.Now().Unix()
  84. var ids []int64
  85. for i := 0; i < 5; i++ {
  86. res, err := m.Insert(ctx, &SysProductMember{
  87. ProductCode: pc,
  88. UserId: randProductMemberUserId(),
  89. MemberType: "MEMBER",
  90. Status: 1,
  91. CreateTime: ts,
  92. UpdateTime: ts,
  93. })
  94. if err != nil {
  95. t.Fatalf("Insert: %v", err)
  96. }
  97. id, _ := res.LastInsertId()
  98. ids = append(ids, id)
  99. }
  100. defer func() {
  101. for _, id := range ids {
  102. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  103. }
  104. }()
  105. list, total, err := m.FindListByProductCode(ctx, pc, 1, 2)
  106. if err != nil {
  107. t.Fatalf("page1: %v", err)
  108. }
  109. if total != 5 || len(list) != 2 {
  110. t.Fatalf("page1 total=%d len=%d", total, len(list))
  111. }
  112. list2, total2, err := m.FindListByProductCode(ctx, pc, 2, 2)
  113. if err != nil {
  114. t.Fatalf("page2: %v", err)
  115. }
  116. if total2 != 5 || len(list2) != 2 {
  117. t.Fatalf("page2 total=%d len=%d", total2, len(list2))
  118. }
  119. list3, total3, err := m.FindListByProductCode(ctx, pc, 3, 2)
  120. if err != nil {
  121. t.Fatalf("page3: %v", err)
  122. }
  123. if total3 != 5 || len(list3) != 1 {
  124. t.Fatalf("page3 total=%d len=%d", total3, len(list3))
  125. }
  126. }
  127. // TC-0477: [REMOVED] FindMapByProductCodeUserIds 作为僵尸接口已在 中被剥离;
  128. // 上层 UserListLogic 改走 FindListByProductMembers 合并查询(见 mock 测试注释)。
  129. // 这里保留 stub 以保持 TC 编号可追溯。
  130. // TC-0336: 多条记录(3条)
  131. func TestSysProductMemberModel_BatchInsert(t *testing.T) {
  132. ctx := context.Background()
  133. conn := testutil.GetTestSqlConn()
  134. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  135. pc := "t_pm_bi_" + testutil.UniqueId()
  136. u1, u2 := randProductMemberUserId(), randProductMemberUserId()
  137. ts := time.Now().Unix()
  138. list := []*SysProductMember{
  139. {Id: 930000001, ProductCode: pc, UserId: u1, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts},
  140. {Id: 930000002, ProductCode: pc, UserId: u2, MemberType: "ADMIN", Status: 1, CreateTime: ts, UpdateTime: ts},
  141. }
  142. if err := m.BatchInsert(ctx, list); err != nil {
  143. t.Fatalf("BatchInsert: %v", err)
  144. }
  145. var rows []struct {
  146. Id int64 `db:"id"`
  147. }
  148. q := "SELECT `id` FROM `sys_product_member` WHERE `productCode` = ? ORDER BY `id`"
  149. if err := conn.QueryRowsCtx(ctx, &rows, q, pc); err != nil {
  150. t.Fatalf("query: %v", err)
  151. }
  152. defer func() {
  153. for _, r := range rows {
  154. testutil.CleanTable(ctx, conn, "sys_product_member", r.Id)
  155. }
  156. }()
  157. if len(rows) != 2 {
  158. t.Fatalf("want 2 rows got %d", len(rows))
  159. }
  160. }
  161. // TC-0312: 唯一索引冲突
  162. func TestSysProductMemberModel_DuplicateConstraint(t *testing.T) {
  163. ctx := context.Background()
  164. conn := testutil.GetTestSqlConn()
  165. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  166. pc := "t_pm_dup_" + testutil.UniqueId()
  167. userId := randProductMemberUserId()
  168. ts := time.Now().Unix()
  169. res, err := m.Insert(ctx, &SysProductMember{ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts})
  170. if err != nil {
  171. t.Fatalf("Insert: %v", err)
  172. }
  173. id, _ := res.LastInsertId()
  174. defer testutil.CleanTable(ctx, conn, "sys_product_member", id)
  175. _, err = m.Insert(ctx, &SysProductMember{ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 1, CreateTime: ts, UpdateTime: ts})
  176. if err == nil {
  177. t.Fatal("second Insert want error")
  178. }
  179. var me *mysql.MySQLError
  180. if !errors.As(err, &me) || me.Number != 1062 {
  181. t.Fatalf("want duplicate key 1062, got %v", err)
  182. }
  183. }
  184. // TC-0319: 记录不存在
  185. func TestSysProductMemberModel_FindOne_NotFound(t *testing.T) {
  186. conn := testutil.GetTestSqlConn()
  187. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  188. _, err := m.FindOne(context.Background(), 999999999999)
  189. if err != ErrNotFound {
  190. t.Fatalf("want ErrNotFound got %v", err)
  191. }
  192. }
  193. // TC-0392: FindOneByProductCodeUserId
  194. func TestSysProductMemberModel_FindOneByProductCodeUserId_NotFound(t *testing.T) {
  195. conn := testutil.GetTestSqlConn()
  196. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  197. _, err := m.FindOneByProductCodeUserId(context.Background(), "notexist_"+testutil.UniqueId(), 999999999)
  198. if err != ErrNotFound {
  199. t.Fatalf("want ErrNotFound got %v", err)
  200. }
  201. }
  202. // TC-0476: 空结果
  203. func TestSysProductMemberModel_FindListByProductCode_Empty(t *testing.T) {
  204. conn := testutil.GetTestSqlConn()
  205. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  206. list, total, err := m.FindListByProductCode(context.Background(), "empty_"+testutil.UniqueId(), 1, 10)
  207. if err != nil {
  208. t.Fatalf("err: %v", err)
  209. }
  210. if total != 0 || len(list) != 0 {
  211. t.Fatalf("want empty got total=%d len=%d", total, len(list))
  212. }
  213. }
  214. // TC-0334: 空列表
  215. func TestSysProductMemberModel_BatchInsert_Empty(t *testing.T) {
  216. conn := testutil.GetTestSqlConn()
  217. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  218. if err := m.BatchInsert(context.Background(), nil); err != nil {
  219. t.Fatalf("nil: %v", err)
  220. }
  221. if err := m.BatchInsert(context.Background(), []*SysProductMember{}); err != nil {
  222. t.Fatalf("empty: %v", err)
  223. }
  224. }
  225. // TC-0353: 空ids
  226. func TestSysProductMemberModel_BatchDelete_Empty(t *testing.T) {
  227. conn := testutil.GetTestSqlConn()
  228. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  229. if err := m.BatchDelete(context.Background(), nil); err != nil {
  230. t.Fatalf("nil: %v", err)
  231. }
  232. if err := m.BatchDelete(context.Background(), []int64{}); err != nil {
  233. t.Fatalf("empty: %v", err)
  234. }
  235. }
  236. // TC-0314: 事务内插入
  237. func TestSysProductMemberModel_InsertWithTx_Normal(t *testing.T) {
  238. ctx := context.Background()
  239. conn := testutil.GetTestSqlConn()
  240. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  241. pc := "t_pm_itx_" + testutil.UniqueId()
  242. userId := randProductMemberUserId()
  243. ts := time.Now().Unix()
  244. var insertedId int64
  245. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  246. res, err := m.InsertWithTx(c, session, &SysProductMember{
  247. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  248. })
  249. if err != nil {
  250. return err
  251. }
  252. insertedId, _ = res.LastInsertId()
  253. return nil
  254. })
  255. if err != nil {
  256. t.Fatalf("TransactCtx: %v", err)
  257. }
  258. defer testutil.CleanTable(ctx, conn, "sys_product_member", insertedId)
  259. got, err := m.FindOne(ctx, insertedId)
  260. if err != nil {
  261. t.Fatalf("FindOne: %v", err)
  262. }
  263. if got.ProductCode != pc || got.UserId != userId {
  264. t.Fatalf("mismatch: %+v", got)
  265. }
  266. }
  267. // TC-0316: 事务回滚后无数据
  268. func TestSysProductMemberModel_InsertWithTx_Rollback(t *testing.T) {
  269. ctx := context.Background()
  270. conn := testutil.GetTestSqlConn()
  271. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  272. pc := "t_pm_irb_" + testutil.UniqueId()
  273. userId := randProductMemberUserId()
  274. ts := time.Now().Unix()
  275. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  276. _, err := m.InsertWithTx(c, session, &SysProductMember{
  277. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  278. })
  279. if err != nil {
  280. return err
  281. }
  282. return errors.New("rollback")
  283. })
  284. if err == nil || err.Error() != "rollback" {
  285. t.Fatalf("want rollback error got %v", err)
  286. }
  287. _, err = m.FindOneByProductCodeUserId(ctx, pc, userId)
  288. if err != ErrNotFound {
  289. t.Fatalf("after rollback want ErrNotFound got %v", err)
  290. }
  291. }
  292. // TC-0326: 记录不存在
  293. func TestSysProductMemberModel_Update_NotFound(t *testing.T) {
  294. conn := testutil.GetTestSqlConn()
  295. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  296. ts := time.Now().Unix()
  297. err := m.Update(context.Background(), &SysProductMember{
  298. Id: 999999999, ProductCode: "nope", UserId: 1, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  299. })
  300. if err != ErrNotFound {
  301. t.Fatalf("want ErrNotFound got %v", err)
  302. }
  303. }
  304. // TC-0327: 事务内更新
  305. func TestSysProductMemberModel_UpdateWithTx(t *testing.T) {
  306. ctx := context.Background()
  307. conn := testutil.GetTestSqlConn()
  308. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  309. pc := "t_pm_utx_" + testutil.UniqueId()
  310. userId := randProductMemberUserId()
  311. ts := time.Now().Unix()
  312. res, err := m.Insert(ctx, &SysProductMember{
  313. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  314. })
  315. if err != nil {
  316. t.Fatalf("Insert: %v", err)
  317. }
  318. id, _ := res.LastInsertId()
  319. defer testutil.CleanTable(ctx, conn, "sys_product_member", id)
  320. newTs := ts + 100
  321. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  322. return m.UpdateWithTx(c, session, &SysProductMember{
  323. Id: id, ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 2, CreateTime: ts, UpdateTime: newTs,
  324. })
  325. })
  326. if err != nil {
  327. t.Fatalf("UpdateWithTx: %v", err)
  328. }
  329. got, err := m.FindOne(ctx, id)
  330. if err != nil {
  331. t.Fatalf("FindOne: %v", err)
  332. }
  333. if got.MemberType != "ADMIN" || got.Status != 2 || got.UpdateTime != newTs {
  334. t.Fatalf("mismatch: %+v", got)
  335. }
  336. }
  337. // TC-0329: 记录不存在
  338. func TestSysProductMemberModel_Delete_NotFound(t *testing.T) {
  339. conn := testutil.GetTestSqlConn()
  340. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  341. err := m.Delete(context.Background(), 999999999)
  342. if err != ErrNotFound {
  343. t.Fatalf("want ErrNotFound got %v", err)
  344. }
  345. }
  346. // TC-0330: 事务内删除
  347. func TestSysProductMemberModel_DeleteWithTx(t *testing.T) {
  348. ctx := context.Background()
  349. conn := testutil.GetTestSqlConn()
  350. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  351. pc := "t_pm_dtx_" + testutil.UniqueId()
  352. userId := randProductMemberUserId()
  353. ts := time.Now().Unix()
  354. res, err := m.Insert(ctx, &SysProductMember{
  355. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  356. })
  357. if err != nil {
  358. t.Fatalf("Insert: %v", err)
  359. }
  360. id, _ := res.LastInsertId()
  361. defer testutil.CleanTable(ctx, conn, "sys_product_member", id)
  362. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  363. return m.DeleteWithTx(c, session, id)
  364. })
  365. if err != nil {
  366. t.Fatalf("DeleteWithTx: %v", err)
  367. }
  368. if _, err := m.FindOne(ctx, id); err != ErrNotFound {
  369. t.Fatalf("after DeleteWithTx want ErrNotFound got %v", err)
  370. }
  371. }
  372. // TC-0331: 正常事务
  373. func TestSysProductMemberModel_TransactCtx_CommitAndRollback(t *testing.T) {
  374. ctx := context.Background()
  375. conn := testutil.GetTestSqlConn()
  376. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  377. pc := "t_pm_txc_" + testutil.UniqueId()
  378. userId := randProductMemberUserId()
  379. ts := time.Now().Unix()
  380. var insertedId int64
  381. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  382. res, err := m.InsertWithTx(c, session, &SysProductMember{
  383. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  384. })
  385. if err != nil {
  386. return err
  387. }
  388. insertedId, _ = res.LastInsertId()
  389. return nil
  390. })
  391. if err != nil {
  392. t.Fatalf("commit: %v", err)
  393. }
  394. defer testutil.CleanTable(ctx, conn, "sys_product_member", insertedId)
  395. got, err := m.FindOne(ctx, insertedId)
  396. if err != nil {
  397. t.Fatalf("FindOne after commit: %v", err)
  398. }
  399. if got.ProductCode != pc {
  400. t.Fatalf("productCode mismatch: %s", got.ProductCode)
  401. }
  402. pc2 := "t_pm_txr_" + testutil.UniqueId()
  403. userId2 := randProductMemberUserId()
  404. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  405. _, err := m.InsertWithTx(c, session, &SysProductMember{
  406. ProductCode: pc2, UserId: userId2, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  407. })
  408. if err != nil {
  409. return err
  410. }
  411. return errors.New("rollback")
  412. })
  413. if err == nil || err.Error() != "rollback" {
  414. t.Fatalf("want rollback error got %v", err)
  415. }
  416. _, err = m.FindOneByProductCodeUserId(ctx, pc2, userId2)
  417. if err != ErrNotFound {
  418. t.Fatalf("after rollback want ErrNotFound got %v", err)
  419. }
  420. }
  421. // TC-0333: 获取表名
  422. func TestSysProductMemberModel_TableName(t *testing.T) {
  423. conn := testutil.GetTestSqlConn()
  424. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  425. if m.TableName() != "`sys_product_member`" {
  426. t.Fatalf("want `sys_product_member` got %s", m.TableName())
  427. }
  428. }
  429. // TC-0335: 单条记录
  430. func TestSysProductMemberModel_BatchInsert_Single(t *testing.T) {
  431. ctx := context.Background()
  432. conn := testutil.GetTestSqlConn()
  433. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  434. pc := "t_pm_bis_" + testutil.UniqueId()
  435. userId := randProductMemberUserId()
  436. ts := time.Now().Unix()
  437. if err := m.BatchInsert(ctx, []*SysProductMember{
  438. {ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts},
  439. }); err != nil {
  440. t.Fatalf("BatchInsert: %v", err)
  441. }
  442. got, err := m.FindOneByProductCodeUserId(ctx, pc, userId)
  443. if err != nil {
  444. t.Fatalf("FindOneByProductCodeUserId: %v", err)
  445. }
  446. defer testutil.CleanTable(ctx, conn, "sys_product_member", got.Id)
  447. if got.MemberType != "MEMBER" {
  448. t.Fatalf("mismatch: %+v", got)
  449. }
  450. }
  451. // TC-0338: 唯一索引冲突
  452. func TestSysProductMemberModel_BatchInsert_UniqueConflict(t *testing.T) {
  453. ctx := context.Background()
  454. conn := testutil.GetTestSqlConn()
  455. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  456. pc := "t_pm_bic_" + testutil.UniqueId()
  457. userId := randProductMemberUserId()
  458. ts := time.Now().Unix()
  459. err := m.BatchInsert(ctx, []*SysProductMember{
  460. {ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts},
  461. {ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 1, CreateTime: ts, UpdateTime: ts},
  462. })
  463. if err == nil {
  464. t.Fatal("want error for duplicate")
  465. }
  466. var me *mysql.MySQLError
  467. if !errors.As(err, &me) || me.Number != 1062 {
  468. t.Fatalf("want duplicate key 1062, got %v", err)
  469. }
  470. }
  471. // TC-0343: 空列表
  472. func TestSysProductMemberModel_BatchUpdate_Empty(t *testing.T) {
  473. conn := testutil.GetTestSqlConn()
  474. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  475. if err := m.BatchUpdate(context.Background(), nil); err != nil {
  476. t.Fatalf("nil: %v", err)
  477. }
  478. if err := m.BatchUpdate(context.Background(), []*SysProductMember{}); err != nil {
  479. t.Fatalf("empty: %v", err)
  480. }
  481. }
  482. // TC-0345: 多条记录(3条)
  483. func TestSysProductMemberModel_BatchUpdate_Multi(t *testing.T) {
  484. ctx := context.Background()
  485. conn := testutil.GetTestSqlConn()
  486. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  487. pc := "t_pm_bum_" + testutil.UniqueId()
  488. u1, u2 := randProductMemberUserId(), randProductMemberUserId()
  489. ts := time.Now().Unix()
  490. res1, err := m.Insert(ctx, &SysProductMember{ProductCode: pc, UserId: u1, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts})
  491. if err != nil {
  492. t.Fatalf("Insert1: %v", err)
  493. }
  494. id1, _ := res1.LastInsertId()
  495. res2, err := m.Insert(ctx, &SysProductMember{ProductCode: pc, UserId: u2, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts})
  496. if err != nil {
  497. t.Fatalf("Insert2: %v", err)
  498. }
  499. id2, _ := res2.LastInsertId()
  500. defer testutil.CleanTable(ctx, conn, "sys_product_member", id1, id2)
  501. newTs := ts + 100
  502. err = m.BatchUpdate(ctx, []*SysProductMember{
  503. {Id: id1, ProductCode: pc, UserId: u1, MemberType: "ADMIN", Status: 2, CreateTime: ts, UpdateTime: newTs},
  504. {Id: id2, ProductCode: pc, UserId: u2, MemberType: "ADMIN", Status: 2, CreateTime: ts, UpdateTime: newTs},
  505. })
  506. if err != nil {
  507. t.Fatalf("BatchUpdate: %v", err)
  508. }
  509. got1, err := m.FindOne(ctx, id1)
  510. if err != nil {
  511. t.Fatalf("FindOne1: %v", err)
  512. }
  513. if got1.MemberType != "ADMIN" || got1.Status != 2 || got1.UpdateTime != newTs {
  514. t.Fatalf("got1 mismatch: %+v", got1)
  515. }
  516. got2, err := m.FindOne(ctx, id2)
  517. if err != nil {
  518. t.Fatalf("FindOne2: %v", err)
  519. }
  520. if got2.MemberType != "ADMIN" || got2.Status != 2 || got2.UpdateTime != newTs {
  521. t.Fatalf("got2 mismatch: %+v", got2)
  522. }
  523. }
  524. // TC-0355: 多个id(3个)
  525. func TestSysProductMemberModel_BatchDelete_Multi(t *testing.T) {
  526. ctx := context.Background()
  527. conn := testutil.GetTestSqlConn()
  528. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  529. pc := "t_pm_bdm_" + testutil.UniqueId()
  530. ts := time.Now().Unix()
  531. var ids []int64
  532. for i := 0; i < 3; i++ {
  533. res, err := m.Insert(ctx, &SysProductMember{
  534. ProductCode: pc, UserId: randProductMemberUserId(), MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  535. })
  536. if err != nil {
  537. t.Fatalf("Insert: %v", err)
  538. }
  539. id, _ := res.LastInsertId()
  540. ids = append(ids, id)
  541. }
  542. defer func() {
  543. for _, id := range ids {
  544. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  545. }
  546. }()
  547. if err := m.BatchDelete(ctx, ids); err != nil {
  548. t.Fatalf("BatchDelete: %v", err)
  549. }
  550. for _, id := range ids {
  551. if _, err := m.FindOne(ctx, id); err != ErrNotFound {
  552. t.Fatalf("id %d should be deleted: %v", id, err)
  553. }
  554. }
  555. }
  556. // TC-0354: 单个id
  557. func TestSysProductMemberModel_BatchDelete_Single(t *testing.T) {
  558. ctx := context.Background()
  559. conn := testutil.GetTestSqlConn()
  560. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  561. pc := "t_pm_bds_" + testutil.UniqueId()
  562. userId := randProductMemberUserId()
  563. ts := time.Now().Unix()
  564. res, err := m.Insert(ctx, &SysProductMember{
  565. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  566. })
  567. if err != nil {
  568. t.Fatalf("Insert: %v", err)
  569. }
  570. id, _ := res.LastInsertId()
  571. defer testutil.CleanTable(ctx, conn, "sys_product_member", id)
  572. if err := m.BatchDelete(ctx, []int64{id}); err != nil {
  573. t.Fatalf("BatchDelete: %v", err)
  574. }
  575. if _, err := m.FindOne(ctx, id); err != ErrNotFound {
  576. t.Fatalf("want ErrNotFound got %v", err)
  577. }
  578. }
  579. // TC-0356: 包含不存在id
  580. func TestSysProductMemberModel_BatchDelete_ContainsNonExist(t *testing.T) {
  581. ctx := context.Background()
  582. conn := testutil.GetTestSqlConn()
  583. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  584. pc := "t_pm_bdn_" + testutil.UniqueId()
  585. userId := randProductMemberUserId()
  586. ts := time.Now().Unix()
  587. res, err := m.Insert(ctx, &SysProductMember{
  588. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  589. })
  590. if err != nil {
  591. t.Fatalf("Insert: %v", err)
  592. }
  593. id, _ := res.LastInsertId()
  594. defer testutil.CleanTable(ctx, conn, "sys_product_member", id)
  595. if err := m.BatchDelete(ctx, []int64{id, 999999999}); err != nil {
  596. t.Fatalf("BatchDelete: %v", err)
  597. }
  598. if _, err := m.FindOne(ctx, id); err != ErrNotFound {
  599. t.Fatalf("want ErrNotFound got %v", err)
  600. }
  601. }
  602. // TC-0341: 正常多条
  603. func TestSysProductMemberModel_BatchInsertWithTx_Normal(t *testing.T) {
  604. ctx := context.Background()
  605. conn := testutil.GetTestSqlConn()
  606. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  607. pc := "t_pm_bitn_" + testutil.UniqueId()
  608. u1, u2 := randProductMemberUserId(), randProductMemberUserId()
  609. ts := time.Now().Unix()
  610. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  611. return m.BatchInsertWithTx(c, session, []*SysProductMember{
  612. {ProductCode: pc, UserId: u1, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts},
  613. {ProductCode: pc, UserId: u2, MemberType: "ADMIN", Status: 1, CreateTime: ts, UpdateTime: ts},
  614. })
  615. })
  616. if err != nil {
  617. t.Fatalf("BatchInsertWithTx: %v", err)
  618. }
  619. got1, err := m.FindOneByProductCodeUserId(ctx, pc, u1)
  620. if err != nil {
  621. t.Fatalf("FindOne u1: %v", err)
  622. }
  623. got2, err := m.FindOneByProductCodeUserId(ctx, pc, u2)
  624. if err != nil {
  625. t.Fatalf("FindOne u2: %v", err)
  626. }
  627. defer testutil.CleanTable(ctx, conn, "sys_product_member", got1.Id, got2.Id)
  628. }
  629. // TC-0340: 空列表
  630. func TestSysProductMemberModel_BatchInsertWithTx_Empty(t *testing.T) {
  631. ctx := context.Background()
  632. conn := testutil.GetTestSqlConn()
  633. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  634. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  635. if err := m.BatchInsertWithTx(c, session, nil); err != nil {
  636. return err
  637. }
  638. return m.BatchInsertWithTx(c, session, []*SysProductMember{})
  639. })
  640. if err != nil {
  641. t.Fatalf("BatchInsertWithTx empty: %v", err)
  642. }
  643. }
  644. // TC-0342: 事务回滚
  645. func TestSysProductMemberModel_BatchInsertWithTx_Rollback(t *testing.T) {
  646. ctx := context.Background()
  647. conn := testutil.GetTestSqlConn()
  648. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  649. pc := "t_pm_bitr_" + testutil.UniqueId()
  650. userId := randProductMemberUserId()
  651. ts := time.Now().Unix()
  652. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  653. if err := m.BatchInsertWithTx(c, session, []*SysProductMember{
  654. {ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts},
  655. }); err != nil {
  656. return err
  657. }
  658. return errors.New("rollback")
  659. })
  660. if err == nil || err.Error() != "rollback" {
  661. t.Fatalf("want rollback error got %v", err)
  662. }
  663. _, err = m.FindOneByProductCodeUserId(ctx, pc, userId)
  664. if err != ErrNotFound {
  665. t.Fatalf("after rollback want ErrNotFound got %v", err)
  666. }
  667. }
  668. // TC-0349: 正常多条
  669. func TestSysProductMemberModel_BatchUpdateWithTx_Normal(t *testing.T) {
  670. ctx := context.Background()
  671. conn := testutil.GetTestSqlConn()
  672. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  673. pc := "t_pm_butn_" + testutil.UniqueId()
  674. u1, u2 := randProductMemberUserId(), randProductMemberUserId()
  675. ts := time.Now().Unix()
  676. res1, err := m.Insert(ctx, &SysProductMember{ProductCode: pc, UserId: u1, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts})
  677. if err != nil {
  678. t.Fatalf("Insert1: %v", err)
  679. }
  680. id1, _ := res1.LastInsertId()
  681. res2, err := m.Insert(ctx, &SysProductMember{ProductCode: pc, UserId: u2, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts})
  682. if err != nil {
  683. t.Fatalf("Insert2: %v", err)
  684. }
  685. id2, _ := res2.LastInsertId()
  686. defer testutil.CleanTable(ctx, conn, "sys_product_member", id1, id2)
  687. newTs := ts + 200
  688. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  689. return m.BatchUpdateWithTx(c, session, []*SysProductMember{
  690. {Id: id1, ProductCode: pc, UserId: u1, MemberType: "ADMIN", Status: 2, CreateTime: ts, UpdateTime: newTs},
  691. {Id: id2, ProductCode: pc, UserId: u2, MemberType: "ADMIN", Status: 2, CreateTime: ts, UpdateTime: newTs},
  692. })
  693. })
  694. if err != nil {
  695. t.Fatalf("BatchUpdateWithTx: %v", err)
  696. }
  697. got1, err := m.FindOne(ctx, id1)
  698. if err != nil {
  699. t.Fatalf("FindOne1: %v", err)
  700. }
  701. if got1.MemberType != "ADMIN" || got1.Status != 2 || got1.UpdateTime != newTs {
  702. t.Fatalf("got1 mismatch: %+v", got1)
  703. }
  704. }
  705. // TC-0348: 空列表
  706. func TestSysProductMemberModel_BatchUpdateWithTx_Empty(t *testing.T) {
  707. ctx := context.Background()
  708. conn := testutil.GetTestSqlConn()
  709. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  710. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  711. if err := m.BatchUpdateWithTx(c, session, nil); err != nil {
  712. return err
  713. }
  714. return m.BatchUpdateWithTx(c, session, []*SysProductMember{})
  715. })
  716. if err != nil {
  717. t.Fatalf("BatchUpdateWithTx empty: %v", err)
  718. }
  719. }
  720. // TC-0358: 正常多条
  721. func TestSysProductMemberModel_BatchDeleteWithTx_Normal(t *testing.T) {
  722. ctx := context.Background()
  723. conn := testutil.GetTestSqlConn()
  724. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  725. pc := "t_pm_bdtn_" + testutil.UniqueId()
  726. ts := time.Now().Unix()
  727. var ids []int64
  728. for i := 0; i < 2; i++ {
  729. res, err := m.Insert(ctx, &SysProductMember{
  730. ProductCode: pc, UserId: randProductMemberUserId(), MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  731. })
  732. if err != nil {
  733. t.Fatalf("Insert: %v", err)
  734. }
  735. id, _ := res.LastInsertId()
  736. ids = append(ids, id)
  737. }
  738. defer func() {
  739. for _, id := range ids {
  740. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  741. }
  742. }()
  743. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  744. return m.BatchDeleteWithTx(c, session, ids)
  745. })
  746. if err != nil {
  747. t.Fatalf("BatchDeleteWithTx: %v", err)
  748. }
  749. for _, id := range ids {
  750. if _, err := m.FindOne(ctx, id); err != ErrNotFound {
  751. t.Fatalf("id %d should be deleted: %v", id, err)
  752. }
  753. }
  754. }
  755. // TC-0357: 空ids
  756. func TestSysProductMemberModel_BatchDeleteWithTx_Empty(t *testing.T) {
  757. ctx := context.Background()
  758. conn := testutil.GetTestSqlConn()
  759. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  760. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  761. if err := m.BatchDeleteWithTx(c, session, nil); err != nil {
  762. return err
  763. }
  764. return m.BatchDeleteWithTx(c, session, []int64{})
  765. })
  766. if err != nil {
  767. t.Fatalf("BatchDeleteWithTx empty: %v", err)
  768. }
  769. }
  770. // TC-0323: 事务内可见性
  771. func TestSysProductMemberModel_FindOneWithTx_InsertThenFind(t *testing.T) {
  772. ctx := context.Background()
  773. conn := testutil.GetTestSqlConn()
  774. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  775. pc := "t_pm_fotx_" + testutil.UniqueId()
  776. userId := randProductMemberUserId()
  777. ts := time.Now().Unix()
  778. var insertedId int64
  779. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  780. res, err := m.InsertWithTx(c, session, &SysProductMember{
  781. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: ts, UpdateTime: ts,
  782. })
  783. if err != nil {
  784. return err
  785. }
  786. insertedId, err = res.LastInsertId()
  787. if err != nil {
  788. return err
  789. }
  790. got, err := m.FindOneWithTx(c, session, insertedId)
  791. if err != nil {
  792. return err
  793. }
  794. assert.Equal(t, pc, got.ProductCode)
  795. assert.Equal(t, userId, got.UserId)
  796. assert.Equal(t, "MEMBER", got.MemberType)
  797. return nil
  798. })
  799. require.NoError(t, err)
  800. defer testutil.CleanTable(ctx, conn, "sys_product_member", insertedId)
  801. }
  802. // TC-0322: 事务内记录不存在
  803. func TestSysProductMemberModel_FindOneWithTx_NotFound(t *testing.T) {
  804. ctx := context.Background()
  805. conn := testutil.GetTestSqlConn()
  806. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  807. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  808. _, err := m.FindOneWithTx(c, session, 999999999999)
  809. require.ErrorIs(t, err, ErrNotFound)
  810. return nil
  811. })
  812. require.NoError(t, err)
  813. }
  814. // TC-0393: FindOneByProductCodeUserIdWithTx
  815. func TestSysProductMemberModel_FindOneByProductCodeUserIdWithTx_InsertThenFind(t *testing.T) {
  816. ctx := context.Background()
  817. conn := testutil.GetTestSqlConn()
  818. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  819. pc := "t_pm_fbytx_" + testutil.UniqueId()
  820. userId := randProductMemberUserId()
  821. ts := time.Now().Unix()
  822. var insertedId int64
  823. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  824. res, err := m.InsertWithTx(c, session, &SysProductMember{
  825. ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 1, CreateTime: ts, UpdateTime: ts,
  826. })
  827. if err != nil {
  828. return err
  829. }
  830. insertedId, err = res.LastInsertId()
  831. if err != nil {
  832. return err
  833. }
  834. got, err := m.FindOneByProductCodeUserIdWithTx(c, session, pc, userId)
  835. if err != nil {
  836. return err
  837. }
  838. assert.Equal(t, insertedId, got.Id)
  839. assert.Equal(t, pc, got.ProductCode)
  840. assert.Equal(t, userId, got.UserId)
  841. return nil
  842. })
  843. require.NoError(t, err)
  844. defer testutil.CleanTable(ctx, conn, "sys_product_member", insertedId)
  845. }
  846. // TC-0394: FindOneByProductCodeUserIdWithTx
  847. func TestSysProductMemberModel_FindOneByProductCodeUserIdWithTx_NotFound(t *testing.T) {
  848. ctx := context.Background()
  849. conn := testutil.GetTestSqlConn()
  850. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  851. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  852. _, err := m.FindOneByProductCodeUserIdWithTx(c, session, "notexist_"+testutil.UniqueId(), 999999999)
  853. require.ErrorIs(t, err, ErrNotFound)
  854. return nil
  855. })
  856. require.NoError(t, err)
  857. }
  858. // TC-0478 / TC-0480: [REMOVED] 参见 TC-0477;方法已随 清理一并移除。
  859. func TestCountOtherActiveAdminsTx_SoleAdmin_ReturnsZero(t *testing.T) {
  860. ctx := context.Background()
  861. conn := testutil.GetTestSqlConn()
  862. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  863. pc := "t_pm_coaa_sole_" + testutil.UniqueId()
  864. adminUser := randProductMemberUserId()
  865. ts := time.Now().Unix()
  866. res, err := m.Insert(ctx, &SysProductMember{
  867. ProductCode: pc, UserId: adminUser,
  868. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  869. CreateTime: ts, UpdateTime: ts,
  870. })
  871. require.NoError(t, err)
  872. adminId, _ := res.LastInsertId()
  873. defer testutil.CleanTable(ctx, conn, "sys_product_member", adminId)
  874. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  875. n, e := m.CountOtherActiveAdminsTx(c, session, pc, adminId)
  876. require.NoError(t, e)
  877. assert.Equal(t, int64(0), n,
  878. "唯一 admin 排除自己后必须为 0,调用方据此才能阻止删除最后一个 admin")
  879. return nil
  880. })
  881. require.NoError(t, err)
  882. }
  883. // TC-0868: 多 admin 场景,排除 A 后返回剩余 backup admin 数量。
  884. func TestCountOtherActiveAdminsTx_MultipleAdmins_ExcludesSelf(t *testing.T) {
  885. ctx := context.Background()
  886. conn := testutil.GetTestSqlConn()
  887. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  888. pc := "t_pm_coaa_multi_" + testutil.UniqueId()
  889. ts := time.Now().Unix()
  890. // 插三个启用 ADMIN + 一个启用 MEMBER + 一个禁用 ADMIN,用来检验 WHERE 条件完整性。
  891. type row struct {
  892. mt string
  893. status int64
  894. }
  895. rows := []row{
  896. {consts.MemberTypeAdmin, consts.StatusEnabled},
  897. {consts.MemberTypeAdmin, consts.StatusEnabled},
  898. {consts.MemberTypeAdmin, consts.StatusEnabled},
  899. {consts.MemberTypeMember, consts.StatusEnabled}, // 不计入
  900. {consts.MemberTypeAdmin, consts.StatusDisabled}, // 不计入
  901. }
  902. ids := make([]int64, 0, len(rows))
  903. for _, r := range rows {
  904. uid := randProductMemberUserId()
  905. res, err := m.Insert(ctx, &SysProductMember{
  906. ProductCode: pc, UserId: uid,
  907. MemberType: r.mt, Status: r.status,
  908. CreateTime: ts, UpdateTime: ts,
  909. })
  910. require.NoError(t, err)
  911. id, _ := res.LastInsertId()
  912. ids = append(ids, id)
  913. }
  914. t.Cleanup(func() {
  915. for _, id := range ids {
  916. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  917. }
  918. })
  919. // 排除 ids[0]:剩下应还有两个启用 ADMIN。
  920. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  921. n, e := m.CountOtherActiveAdminsTx(c, session, pc, ids[0])
  922. require.NoError(t, e)
  923. assert.Equal(t, int64(2), n,
  924. "MEMBER 与 Disabled 行不得被计入;排除自己后剩余 admin 数必须等于 2")
  925. return nil
  926. })
  927. require.NoError(t, err)
  928. }
  929. // TC-0869: 排除一个根本不存在的 id,CountOtherActiveAdminsTx 应直接返回总数 2。
  930. //
  931. // 本轮已经把冗余的 CountActiveAdminsTx(不带 Other)从接口删掉,自洽性校验从
  932. //
  933. // CountOther(-1) == CountActive(pc)
  934. //
  935. // 收紧为
  936. //
  937. // CountOther(-1) == 已知播种总数
  938. //
  939. // 语义等价,但不再依赖已删除的镜像方法(收敛 surface area)。
  940. func TestCountOtherActiveAdminsTx_NonExistentExclude_EqualsTotal(t *testing.T) {
  941. ctx := context.Background()
  942. conn := testutil.GetTestSqlConn()
  943. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  944. pc := "t_pm_coaa_none_" + testutil.UniqueId()
  945. ts := time.Now().Unix()
  946. var ids []int64
  947. for i := 0; i < 2; i++ {
  948. res, err := m.Insert(ctx, &SysProductMember{
  949. ProductCode: pc, UserId: randProductMemberUserId(),
  950. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  951. CreateTime: ts, UpdateTime: ts,
  952. })
  953. require.NoError(t, err)
  954. id, _ := res.LastInsertId()
  955. ids = append(ids, id)
  956. }
  957. t.Cleanup(func() {
  958. for _, id := range ids {
  959. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  960. }
  961. })
  962. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  963. other, e := m.CountOtherActiveAdminsTx(c, session, pc, -1) // -1 不存在
  964. require.NoError(t, e)
  965. assert.Equal(t, int64(2), other,
  966. "excludeId 不存在时 CountOtherActiveAdminsTx 应等于产品内 active admin 总数")
  967. return nil
  968. })
  969. require.NoError(t, err)
  970. }
  971. // TC-0870: 空 productCode 不会串库 —— 不同产品线互不影响。
  972. func TestCountOtherActiveAdminsTx_ScopedByProductCode(t *testing.T) {
  973. ctx := context.Background()
  974. conn := testutil.GetTestSqlConn()
  975. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  976. ts := time.Now().Unix()
  977. pcA := "t_pm_coaa_A_" + testutil.UniqueId()
  978. pcB := "t_pm_coaa_B_" + testutil.UniqueId()
  979. // 产品 A 有 1 个 admin(自己),排除后应为 0;产品 B 有 2 个 admin,这条查询不应拉到产品 B。
  980. resA, err := m.Insert(ctx, &SysProductMember{
  981. ProductCode: pcA, UserId: randProductMemberUserId(),
  982. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  983. CreateTime: ts, UpdateTime: ts,
  984. })
  985. require.NoError(t, err)
  986. aId, _ := resA.LastInsertId()
  987. defer testutil.CleanTable(ctx, conn, "sys_product_member", aId)
  988. var bIds []int64
  989. for i := 0; i < 2; i++ {
  990. r, err := m.Insert(ctx, &SysProductMember{
  991. ProductCode: pcB, UserId: randProductMemberUserId(),
  992. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  993. CreateTime: ts, UpdateTime: ts,
  994. })
  995. require.NoError(t, err)
  996. id, _ := r.LastInsertId()
  997. bIds = append(bIds, id)
  998. }
  999. t.Cleanup(func() {
  1000. for _, id := range bIds {
  1001. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  1002. }
  1003. })
  1004. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  1005. n, e := m.CountOtherActiveAdminsTx(c, session, pcA, aId)
  1006. require.NoError(t, e)
  1007. assert.Equal(t, int64(0), n,
  1008. "pcA 的排除计数必须只看 pcA,绝不能把 pcB 的 2 个 admin 误计入")
  1009. return nil
  1010. })
  1011. require.NoError(t, err)
  1012. }
  1013. // TC-1110: 事务内按 id 读到最新行并持有 S 锁。只验证数据契约;锁效果由 TC-1105
  1014. // 的 setUserPerms TOCTOU 集成用例端到端覆盖。
  1015. func TestSysProductMemberModel_FindOneForShareTx_ReadsInsertedRow(t *testing.T) {
  1016. ctx := context.Background()
  1017. conn := testutil.GetTestSqlConn()
  1018. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  1019. pc := "t_pm_fsharetx_" + testutil.UniqueId()
  1020. userId := randProductMemberUserId()
  1021. ts := time.Now().Unix()
  1022. res, err := m.Insert(ctx, &SysProductMember{
  1023. ProductCode: pc, UserId: userId,
  1024. MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled,
  1025. CreateTime: ts, UpdateTime: ts,
  1026. })
  1027. require.NoError(t, err)
  1028. id, _ := res.LastInsertId()
  1029. defer testutil.CleanTable(ctx, conn, "sys_product_member", id)
  1030. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  1031. got, e := m.FindOneForShareTx(c, session, id)
  1032. require.NoError(t, e, "FindOneForShareTx 在已有行上必须成功")
  1033. require.NotNil(t, got)
  1034. assert.Equal(t, id, got.Id)
  1035. assert.Equal(t, pc, got.ProductCode)
  1036. assert.Equal(t, userId, got.UserId)
  1037. assert.Equal(t, consts.MemberTypeMember, got.MemberType)
  1038. assert.Equal(t, int64(consts.StatusEnabled), got.Status,
  1039. "S 锁读必须反映事务开始时的最新 Status,不得返回缓存中的旧值")
  1040. return nil
  1041. })
  1042. require.NoError(t, err)
  1043. }
  1044. // TC-1111: 不存在的 id 必须返回 sqlx.ErrNotFound,便于 setUserPerms 把"目标成员被并发删"
  1045. // 映射成 409,而不是被误吞为 5xx。
  1046. func TestSysProductMemberModel_FindOneForShareTx_NotFound(t *testing.T) {
  1047. ctx := context.Background()
  1048. conn := testutil.GetTestSqlConn()
  1049. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  1050. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  1051. _, e := m.FindOneForShareTx(c, session, 99999999)
  1052. require.Error(t, e)
  1053. require.True(t, errors.Is(e, sqlx.ErrNotFound),
  1054. "FindOneForShareTx 必须把缺失映射为 sqlx.ErrNotFound;"+
  1055. "否则 setUserPerms 无法识别'成员被并发删除'路径,会被误吞为 500")
  1056. return nil
  1057. })
  1058. require.NoError(t, err)
  1059. }