user_attribute_handler.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. package admin
  2. import (
  3. "encoding/json"
  4. "strconv"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. )
  10. // UserAttributeHandler handles user attribute management
  11. type UserAttributeHandler struct {
  12. attrService *service.UserAttributeService
  13. }
  14. // NewUserAttributeHandler creates a new handler
  15. func NewUserAttributeHandler(attrService *service.UserAttributeService) *UserAttributeHandler {
  16. return &UserAttributeHandler{attrService: attrService}
  17. }
  18. // --- Request/Response DTOs ---
  19. // CreateAttributeDefinitionRequest represents create attribute definition request
  20. type CreateAttributeDefinitionRequest struct {
  21. Key string `json:"key" binding:"required,min=1,max=100"`
  22. Name string `json:"name" binding:"required,min=1,max=255"`
  23. Description string `json:"description"`
  24. Type string `json:"type" binding:"required"`
  25. Options []service.UserAttributeOption `json:"options"`
  26. Required bool `json:"required"`
  27. Validation service.UserAttributeValidation `json:"validation"`
  28. Placeholder string `json:"placeholder"`
  29. Enabled bool `json:"enabled"`
  30. }
  31. // UpdateAttributeDefinitionRequest represents update attribute definition request
  32. type UpdateAttributeDefinitionRequest struct {
  33. Name *string `json:"name"`
  34. Description *string `json:"description"`
  35. Type *string `json:"type"`
  36. Options *[]service.UserAttributeOption `json:"options"`
  37. Required *bool `json:"required"`
  38. Validation *service.UserAttributeValidation `json:"validation"`
  39. Placeholder *string `json:"placeholder"`
  40. Enabled *bool `json:"enabled"`
  41. }
  42. // ReorderRequest represents reorder attribute definitions request
  43. type ReorderRequest struct {
  44. IDs []int64 `json:"ids" binding:"required"`
  45. }
  46. // UpdateUserAttributesRequest represents update user attributes request
  47. type UpdateUserAttributesRequest struct {
  48. Values map[int64]string `json:"values" binding:"required"`
  49. }
  50. // BatchGetUserAttributesRequest represents batch get user attributes request
  51. type BatchGetUserAttributesRequest struct {
  52. UserIDs []int64 `json:"user_ids" binding:"required"`
  53. }
  54. // BatchUserAttributesResponse represents batch user attributes response
  55. type BatchUserAttributesResponse struct {
  56. // Map of userID -> map of attributeID -> value
  57. Attributes map[int64]map[int64]string `json:"attributes"`
  58. }
  59. var userAttributesBatchCache = newSnapshotCache(30 * time.Second)
  60. // AttributeDefinitionResponse represents attribute definition response
  61. type AttributeDefinitionResponse struct {
  62. ID int64 `json:"id"`
  63. Key string `json:"key"`
  64. Name string `json:"name"`
  65. Description string `json:"description"`
  66. Type string `json:"type"`
  67. Options []service.UserAttributeOption `json:"options"`
  68. Required bool `json:"required"`
  69. Validation service.UserAttributeValidation `json:"validation"`
  70. Placeholder string `json:"placeholder"`
  71. DisplayOrder int `json:"display_order"`
  72. Enabled bool `json:"enabled"`
  73. CreatedAt string `json:"created_at"`
  74. UpdatedAt string `json:"updated_at"`
  75. }
  76. // AttributeValueResponse represents attribute value response
  77. type AttributeValueResponse struct {
  78. ID int64 `json:"id"`
  79. UserID int64 `json:"user_id"`
  80. AttributeID int64 `json:"attribute_id"`
  81. Value string `json:"value"`
  82. CreatedAt string `json:"created_at"`
  83. UpdatedAt string `json:"updated_at"`
  84. }
  85. // --- Helpers ---
  86. func defToResponse(def *service.UserAttributeDefinition) *AttributeDefinitionResponse {
  87. return &AttributeDefinitionResponse{
  88. ID: def.ID,
  89. Key: def.Key,
  90. Name: def.Name,
  91. Description: def.Description,
  92. Type: string(def.Type),
  93. Options: def.Options,
  94. Required: def.Required,
  95. Validation: def.Validation,
  96. Placeholder: def.Placeholder,
  97. DisplayOrder: def.DisplayOrder,
  98. Enabled: def.Enabled,
  99. CreatedAt: def.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
  100. UpdatedAt: def.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
  101. }
  102. }
  103. func valueToResponse(val *service.UserAttributeValue) *AttributeValueResponse {
  104. return &AttributeValueResponse{
  105. ID: val.ID,
  106. UserID: val.UserID,
  107. AttributeID: val.AttributeID,
  108. Value: val.Value,
  109. CreatedAt: val.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
  110. UpdatedAt: val.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
  111. }
  112. }
  113. // --- Handlers ---
  114. // ListDefinitions lists all attribute definitions
  115. // GET /admin/user-attributes
  116. func (h *UserAttributeHandler) ListDefinitions(c *gin.Context) {
  117. enabledOnly := c.Query("enabled") == "true"
  118. defs, err := h.attrService.ListDefinitions(c.Request.Context(), enabledOnly)
  119. if err != nil {
  120. response.ErrorFrom(c, err)
  121. return
  122. }
  123. out := make([]*AttributeDefinitionResponse, 0, len(defs))
  124. for i := range defs {
  125. out = append(out, defToResponse(&defs[i]))
  126. }
  127. response.Success(c, out)
  128. }
  129. // CreateDefinition creates a new attribute definition
  130. // POST /admin/user-attributes
  131. func (h *UserAttributeHandler) CreateDefinition(c *gin.Context) {
  132. var req CreateAttributeDefinitionRequest
  133. if err := c.ShouldBindJSON(&req); err != nil {
  134. response.BadRequest(c, "Invalid request: "+err.Error())
  135. return
  136. }
  137. def, err := h.attrService.CreateDefinition(c.Request.Context(), service.CreateAttributeDefinitionInput{
  138. Key: req.Key,
  139. Name: req.Name,
  140. Description: req.Description,
  141. Type: service.UserAttributeType(req.Type),
  142. Options: req.Options,
  143. Required: req.Required,
  144. Validation: req.Validation,
  145. Placeholder: req.Placeholder,
  146. Enabled: req.Enabled,
  147. })
  148. if err != nil {
  149. response.ErrorFrom(c, err)
  150. return
  151. }
  152. response.Success(c, defToResponse(def))
  153. }
  154. // UpdateDefinition updates an attribute definition
  155. // PUT /admin/user-attributes/:id
  156. func (h *UserAttributeHandler) UpdateDefinition(c *gin.Context) {
  157. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  158. if err != nil {
  159. response.BadRequest(c, "Invalid attribute ID")
  160. return
  161. }
  162. var req UpdateAttributeDefinitionRequest
  163. if err := c.ShouldBindJSON(&req); err != nil {
  164. response.BadRequest(c, "Invalid request: "+err.Error())
  165. return
  166. }
  167. input := service.UpdateAttributeDefinitionInput{
  168. Name: req.Name,
  169. Description: req.Description,
  170. Options: req.Options,
  171. Required: req.Required,
  172. Validation: req.Validation,
  173. Placeholder: req.Placeholder,
  174. Enabled: req.Enabled,
  175. }
  176. if req.Type != nil {
  177. t := service.UserAttributeType(*req.Type)
  178. input.Type = &t
  179. }
  180. def, err := h.attrService.UpdateDefinition(c.Request.Context(), id, input)
  181. if err != nil {
  182. response.ErrorFrom(c, err)
  183. return
  184. }
  185. response.Success(c, defToResponse(def))
  186. }
  187. // DeleteDefinition deletes an attribute definition
  188. // DELETE /admin/user-attributes/:id
  189. func (h *UserAttributeHandler) DeleteDefinition(c *gin.Context) {
  190. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  191. if err != nil {
  192. response.BadRequest(c, "Invalid attribute ID")
  193. return
  194. }
  195. if err := h.attrService.DeleteDefinition(c.Request.Context(), id); err != nil {
  196. response.ErrorFrom(c, err)
  197. return
  198. }
  199. response.Success(c, gin.H{"message": "Attribute definition deleted successfully"})
  200. }
  201. // ReorderDefinitions reorders attribute definitions
  202. // PUT /admin/user-attributes/reorder
  203. func (h *UserAttributeHandler) ReorderDefinitions(c *gin.Context) {
  204. var req ReorderRequest
  205. if err := c.ShouldBindJSON(&req); err != nil {
  206. response.BadRequest(c, "Invalid request: "+err.Error())
  207. return
  208. }
  209. // Convert IDs array to orders map (position in array = display_order)
  210. orders := make(map[int64]int, len(req.IDs))
  211. for i, id := range req.IDs {
  212. orders[id] = i
  213. }
  214. if err := h.attrService.ReorderDefinitions(c.Request.Context(), orders); err != nil {
  215. response.ErrorFrom(c, err)
  216. return
  217. }
  218. response.Success(c, gin.H{"message": "Reorder successful"})
  219. }
  220. // GetUserAttributes gets a user's attribute values
  221. // GET /admin/users/:id/attributes
  222. func (h *UserAttributeHandler) GetUserAttributes(c *gin.Context) {
  223. userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  224. if err != nil {
  225. response.BadRequest(c, "Invalid user ID")
  226. return
  227. }
  228. values, err := h.attrService.GetUserAttributes(c.Request.Context(), userID)
  229. if err != nil {
  230. response.ErrorFrom(c, err)
  231. return
  232. }
  233. out := make([]*AttributeValueResponse, 0, len(values))
  234. for i := range values {
  235. out = append(out, valueToResponse(&values[i]))
  236. }
  237. response.Success(c, out)
  238. }
  239. // UpdateUserAttributes updates a user's attribute values
  240. // PUT /admin/users/:id/attributes
  241. func (h *UserAttributeHandler) UpdateUserAttributes(c *gin.Context) {
  242. userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  243. if err != nil {
  244. response.BadRequest(c, "Invalid user ID")
  245. return
  246. }
  247. var req UpdateUserAttributesRequest
  248. if err := c.ShouldBindJSON(&req); err != nil {
  249. response.BadRequest(c, "Invalid request: "+err.Error())
  250. return
  251. }
  252. inputs := make([]service.UpdateUserAttributeInput, 0, len(req.Values))
  253. for attrID, value := range req.Values {
  254. inputs = append(inputs, service.UpdateUserAttributeInput{
  255. AttributeID: attrID,
  256. Value: value,
  257. })
  258. }
  259. if err := h.attrService.UpdateUserAttributes(c.Request.Context(), userID, inputs); err != nil {
  260. response.ErrorFrom(c, err)
  261. return
  262. }
  263. // Return updated values
  264. values, err := h.attrService.GetUserAttributes(c.Request.Context(), userID)
  265. if err != nil {
  266. response.ErrorFrom(c, err)
  267. return
  268. }
  269. out := make([]*AttributeValueResponse, 0, len(values))
  270. for i := range values {
  271. out = append(out, valueToResponse(&values[i]))
  272. }
  273. response.Success(c, out)
  274. }
  275. // GetBatchUserAttributes gets attribute values for multiple users
  276. // POST /admin/user-attributes/batch
  277. func (h *UserAttributeHandler) GetBatchUserAttributes(c *gin.Context) {
  278. var req BatchGetUserAttributesRequest
  279. if err := c.ShouldBindJSON(&req); err != nil {
  280. response.BadRequest(c, "Invalid request: "+err.Error())
  281. return
  282. }
  283. userIDs := normalizeInt64IDList(req.UserIDs)
  284. if len(userIDs) == 0 {
  285. response.Success(c, BatchUserAttributesResponse{Attributes: map[int64]map[int64]string{}})
  286. return
  287. }
  288. keyRaw, _ := json.Marshal(struct {
  289. UserIDs []int64 `json:"user_ids"`
  290. }{
  291. UserIDs: userIDs,
  292. })
  293. cacheKey := string(keyRaw)
  294. if cached, ok := userAttributesBatchCache.Get(cacheKey); ok {
  295. c.Header("X-Snapshot-Cache", "hit")
  296. response.Success(c, cached.Payload)
  297. return
  298. }
  299. attrs, err := h.attrService.GetBatchUserAttributes(c.Request.Context(), userIDs)
  300. if err != nil {
  301. response.ErrorFrom(c, err)
  302. return
  303. }
  304. payload := BatchUserAttributesResponse{Attributes: attrs}
  305. userAttributesBatchCache.Set(cacheKey, payload)
  306. c.Header("X-Snapshot-Cache", "miss")
  307. response.Success(c, payload)
  308. }