diff --git a/docs/docs.go b/docs/docs.go index bad5323..5197694 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -922,6 +922,60 @@ const docTemplate = `{ } } }, + "/admin/messages/latest": { + "get": { + "description": "管理端根据appid获取最新消息记录,包含已读未读状态,访问时自动标记为已读", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.小程序" + ], + "summary": "根据appid获取最新消息记录", + "parameters": [ + { + "type": "string", + "description": "小程序ID", + "name": "app_id", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 1, + "description": "当前页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "每页返回的数据量,最多 100 条", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.latestMessageByAppIdResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/admin/rel_app/{id}": { "put": { "security": [ @@ -1109,6 +1163,52 @@ const docTemplate = `{ } } }, + "/api/wechat/miniprogram/login": { + "post": { + "description": "通过AppID和code获取用户的openid和session_key", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "微信" + ], + "summary": "小程序登录", + "parameters": [ + { + "description": "请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/wechat.miniprogramLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wechat.miniprogramLoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/wechat/qrcode": { "post": { "description": "根据 AppID、AppSecret 和页面路径生成微信小程序二维码", @@ -1216,6 +1316,46 @@ const docTemplate = `{ } } }, + "/app/messages/read": { + "post": { + "description": "标记指定消息为已读状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户端" + ], + "summary": "标记消息为已读", + "parameters": [ + { + "description": "请求参数", + "name": "RequestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/message.markMessageReadRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/message.markMessageReadResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/app/send_message": { "post": { "description": "用户发送消息", @@ -1662,6 +1802,74 @@ const docTemplate = `{ } } }, + "app.latestMessageByAppIdResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.latestMessageData" + } + }, + "page": { + "description": "当前页码", + "type": "integer" + }, + "page_size": { + "description": "每页返回的数据量", + "type": "integer" + }, + "total": { + "description": "符合查询条件的总记录数", + "type": "integer" + } + } + }, + "app.latestMessageData": { + "type": "object", + "properties": { + "content": { + "description": "消息内容", + "type": "string" + }, + "is_read": { + "description": "是否已读(0:未读 1:已读)", + "type": "integer" + }, + "message_id": { + "description": "消息ID", + "type": "integer" + }, + "msg_type": { + "description": "消息类型(1:文本 2:图片)", + "type": "integer" + }, + "receiver_id": { + "description": "接收人ID", + "type": "string" + }, + "send_time": { + "description": "发送时间", + "type": "string" + }, + "sender_avatar": { + "description": "发送人头像", + "type": "string" + }, + "sender_id": { + "description": "发送人ID", + "type": "string" + }, + "sender_name": { + "description": "发送人昵称", + "type": "string" + }, + "unread_count": { + "description": "未读数量", + "type": "integer" + } + } + }, "app.listData": { "type": "object", "properties": { @@ -2156,6 +2364,36 @@ const docTemplate = `{ } } }, + "message.markMessageReadRequest": { + "type": "object", + "required": [ + "app_id", + "message_id", + "user_id" + ], + "properties": { + "app_id": { + "description": "小程序ID", + "type": "string" + }, + "message_id": { + "description": "消息ID", + "type": "integer" + }, + "user_id": { + "description": "用户ID", + "type": "string" + } + } + }, + "message.markMessageReadResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, "message.userSendMessageRequest": { "type": "object", "required": [ @@ -2246,6 +2484,46 @@ const docTemplate = `{ "type": "boolean" } } + }, + "wechat.miniprogramLoginRequest": { + "type": "object", + "required": [ + "app_id", + "js_code" + ], + "properties": { + "app_id": { + "description": "小程序AppID", + "type": "string" + }, + "js_code": { + "description": "登录时获取的code", + "type": "string" + } + } + }, + "wechat.miniprogramLoginResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "openid": { + "description": "用户唯一标识", + "type": "string" + }, + "session_key": { + "description": "会话密钥", + "type": "string" + }, + "success": { + "type": "boolean" + }, + "unionid": { + "description": "用户在开放平台的唯一标识符", + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index 139bddf..e20e8bd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -914,6 +914,60 @@ } } }, + "/admin/messages/latest": { + "get": { + "description": "管理端根据appid获取最新消息记录,包含已读未读状态,访问时自动标记为已读", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.小程序" + ], + "summary": "根据appid获取最新消息记录", + "parameters": [ + { + "type": "string", + "description": "小程序ID", + "name": "app_id", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 1, + "description": "当前页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "每页返回的数据量,最多 100 条", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.latestMessageByAppIdResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/admin/rel_app/{id}": { "put": { "security": [ @@ -1101,6 +1155,52 @@ } } }, + "/api/wechat/miniprogram/login": { + "post": { + "description": "通过AppID和code获取用户的openid和session_key", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "微信" + ], + "summary": "小程序登录", + "parameters": [ + { + "description": "请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/wechat.miniprogramLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wechat.miniprogramLoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/wechat/qrcode": { "post": { "description": "根据 AppID、AppSecret 和页面路径生成微信小程序二维码", @@ -1208,6 +1308,46 @@ } } }, + "/app/messages/read": { + "post": { + "description": "标记指定消息为已读状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户端" + ], + "summary": "标记消息为已读", + "parameters": [ + { + "description": "请求参数", + "name": "RequestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/message.markMessageReadRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/message.markMessageReadResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/app/send_message": { "post": { "description": "用户发送消息", @@ -1654,6 +1794,74 @@ } } }, + "app.latestMessageByAppIdResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.latestMessageData" + } + }, + "page": { + "description": "当前页码", + "type": "integer" + }, + "page_size": { + "description": "每页返回的数据量", + "type": "integer" + }, + "total": { + "description": "符合查询条件的总记录数", + "type": "integer" + } + } + }, + "app.latestMessageData": { + "type": "object", + "properties": { + "content": { + "description": "消息内容", + "type": "string" + }, + "is_read": { + "description": "是否已读(0:未读 1:已读)", + "type": "integer" + }, + "message_id": { + "description": "消息ID", + "type": "integer" + }, + "msg_type": { + "description": "消息类型(1:文本 2:图片)", + "type": "integer" + }, + "receiver_id": { + "description": "接收人ID", + "type": "string" + }, + "send_time": { + "description": "发送时间", + "type": "string" + }, + "sender_avatar": { + "description": "发送人头像", + "type": "string" + }, + "sender_id": { + "description": "发送人ID", + "type": "string" + }, + "sender_name": { + "description": "发送人昵称", + "type": "string" + }, + "unread_count": { + "description": "未读数量", + "type": "integer" + } + } + }, "app.listData": { "type": "object", "properties": { @@ -2148,6 +2356,36 @@ } } }, + "message.markMessageReadRequest": { + "type": "object", + "required": [ + "app_id", + "message_id", + "user_id" + ], + "properties": { + "app_id": { + "description": "小程序ID", + "type": "string" + }, + "message_id": { + "description": "消息ID", + "type": "integer" + }, + "user_id": { + "description": "用户ID", + "type": "string" + } + } + }, + "message.markMessageReadResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, "message.userSendMessageRequest": { "type": "object", "required": [ @@ -2238,6 +2476,46 @@ "type": "boolean" } } + }, + "wechat.miniprogramLoginRequest": { + "type": "object", + "required": [ + "app_id", + "js_code" + ], + "properties": { + "app_id": { + "description": "小程序AppID", + "type": "string" + }, + "js_code": { + "description": "登录时获取的code", + "type": "string" + } + } + }, + "wechat.miniprogramLoginResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "openid": { + "description": "用户唯一标识", + "type": "string" + }, + "session_key": { + "description": "会话密钥", + "type": "string" + }, + "success": { + "type": "boolean" + }, + "unionid": { + "description": "用户在开放平台的唯一标识符", + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 715df64..28a683e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -257,6 +257,55 @@ definitions: description: 提示信息 type: string type: object + app.latestMessageByAppIdResponse: + properties: + list: + items: + $ref: '#/definitions/app.latestMessageData' + type: array + page: + description: 当前页码 + type: integer + page_size: + description: 每页返回的数据量 + type: integer + total: + description: 符合查询条件的总记录数 + type: integer + type: object + app.latestMessageData: + properties: + content: + description: 消息内容 + type: string + is_read: + description: 是否已读(0:未读 1:已读) + type: integer + message_id: + description: 消息ID + type: integer + msg_type: + description: 消息类型(1:文本 2:图片) + type: integer + receiver_id: + description: 接收人ID + type: string + send_time: + description: 发送时间 + type: string + sender_avatar: + description: 发送人头像 + type: string + sender_id: + description: 发送人ID + type: string + sender_name: + description: 发送人昵称 + type: string + unread_count: + description: 未读数量 + type: integer + type: object app.listData: properties: app_id: @@ -607,6 +656,27 @@ definitions: description: 发送人昵称 type: string type: object + message.markMessageReadRequest: + properties: + app_id: + description: 小程序ID + type: string + message_id: + description: 消息ID + type: integer + user_id: + description: 用户ID + type: string + required: + - app_id + - message_id + - user_id + type: object + message.markMessageReadResponse: + properties: + message: + type: string + type: object message.userSendMessageRequest: properties: app_id: @@ -672,6 +742,34 @@ definitions: success: type: boolean type: object + wechat.miniprogramLoginRequest: + properties: + app_id: + description: 小程序AppID + type: string + js_code: + description: 登录时获取的code + type: string + required: + - app_id + - js_code + type: object + wechat.miniprogramLoginResponse: + properties: + message: + type: string + openid: + description: 用户唯一标识 + type: string + session_key: + description: 会话密钥 + type: string + success: + type: boolean + unionid: + description: 用户在开放平台的唯一标识符 + type: string + type: object info: contact: {} title: mini-chat 接口文档 @@ -1294,6 +1392,43 @@ paths: summary: 获取消息日志 tags: - 管理端.小程序 + /admin/messages/latest: + get: + consumes: + - application/json + description: 管理端根据appid获取最新消息记录,包含已读未读状态,访问时自动标记为已读 + parameters: + - description: 小程序ID + in: query + name: app_id + required: true + type: string + - default: 1 + description: 当前页码 + in: query + name: page + required: true + type: integer + - default: 20 + description: 每页返回的数据量,最多 100 条 + in: query + name: page_size + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app.latestMessageByAppIdResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 根据appid获取最新消息记录 + tags: + - 管理端.小程序 /admin/rel_app/{id}: put: consumes: @@ -1380,6 +1515,36 @@ paths: summary: 上传图片 tags: - 通用 + /api/wechat/miniprogram/login: + post: + consumes: + - application/json + description: 通过AppID和code获取用户的openid和session_key + parameters: + - description: 请求参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/wechat.miniprogramLoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/wechat.miniprogramLoginResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/code.Failure' + summary: 小程序登录 + tags: + - 微信 /api/wechat/qrcode: post: consumes: @@ -1452,6 +1617,32 @@ paths: summary: 获取消息日志 tags: - 用户端 + /app/messages/read: + post: + consumes: + - application/json + description: 标记指定消息为已读状态 + parameters: + - description: 请求参数 + in: body + name: RequestBody + required: true + schema: + $ref: '#/definitions/message.markMessageReadRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/message.markMessageReadResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 标记消息为已读 + tags: + - 用户端 /app/send_message: post: consumes: diff --git a/internal/api/app/app_latest_messages.go b/internal/api/app/app_latest_messages.go new file mode 100644 index 0000000..f1073cf --- /dev/null +++ b/internal/api/app/app_latest_messages.go @@ -0,0 +1,189 @@ +package app + +import ( + "fmt" + "net/http" + "time" + + "mini-chat/internal/code" + "mini-chat/internal/pkg/core" + "mini-chat/internal/pkg/timeutil" + "mini-chat/internal/pkg/validation" + "mini-chat/internal/repository/mysql/model" +) + +type latestMessageByAppIdRequest struct { + AppID string `form:"app_id" binding:"required"` // 小程序ID + Page int `form:"page"` // 当前页码,默认1 + PageSize int `form:"page_size"` // 每页返回的数据量,默认20 +} + +type latestMessageData struct { + MessageID int32 `json:"message_id"` // 消息ID + SendTime string `json:"send_time"` // 发送时间 + SenderID string `json:"sender_id"` // 发送人ID + SenderName string `json:"sender_name"` // 发送人昵称 + SenderAvatar string `json:"sender_avatar"` // 发送人头像 + ReceiverID string `json:"receiver_id"` // 接收人ID + Content string `json:"content"` // 消息内容 + MsgType int32 `json:"msg_type"` // 消息类型(1:文本 2:图片) + IsRead int32 `json:"is_read"` // 是否已读(0:未读 1:已读) + UnreadCount int64 `json:"unread_count"` // 未读数量 +} + +type latestMessageByAppIdResponse struct { + Page int `json:"page"` // 当前页码 + PageSize int `json:"page_size"` // 每页返回的数据量 + Total int64 `json:"total"` // 符合查询条件的总记录数 + List []latestMessageData `json:"list"` +} + +// LatestMessageByAppId 根据appid获取最新消息记录 +// @Summary 根据appid获取最新消息记录 +// @Description 管理端根据appid获取最新消息记录,包含已读未读状态,访问时自动标记为已读 +// @Tags 管理端.小程序 +// @Accept json +// @Produce json +// @Param app_id query string true "小程序ID" +// @Param page query int true "当前页码" default(1) +// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20) +// @Success 200 {object} latestMessageByAppIdResponse +// @Failure 400 {object} code.Failure +// @Router /admin/messages/latest [get] +func (h *handler) LatestMessageByAppId() core.HandlerFunc { + return func(ctx core.Context) { + req := new(latestMessageByAppIdRequest) + res := new(latestMessageByAppIdResponse) + if err := ctx.ShouldBindForm(req); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + validation.Error(err)), + ) + return + } + + if req.Page == 0 { + req.Page = 1 + } + + if req.PageSize == 0 { + req.PageSize = 20 + } + + if req.PageSize > 100 { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListMessageError, + fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListMessageError)), + )) + return + } + + query := h.readDB.AppMessageLog.WithContext(ctx.RequestContext()). + Where(h.readDB.AppMessageLog.AppID.Eq(req.AppID)) + + // 查询总数 + total, err := query.Count() + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListMessageError, + fmt.Sprintf("%s:%s", code.Text(code.ListMessageError), err.Error())), + ) + return + } + + // 分页查询指定小程序的最新消息 + resultData, err := query. + Order(h.readDB.AppMessageLog.SendTime.Desc()). + Offset((req.Page - 1) * req.PageSize). + Limit(req.PageSize). + Find() + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListMessageError, + fmt.Sprintf("%s:%s", code.Text(code.ListMessageError), err.Error())), + ) + return + } + + // 自动标记该appid下的所有消息为已读(管理端访问时) + // 这里我们为每个消息的接收者创建或更新已读状态 + for _, message := range resultData { + // 检查是否已存在已读状态记录 + existingStatus, _ := h.readDB.AppMessageReadStatus.WithContext(ctx.RequestContext()). + Where(h.readDB.AppMessageReadStatus.AppID.Eq(req.AppID)). + Where(h.readDB.AppMessageReadStatus.MessageID.Eq(message.ID)). + Where(h.readDB.AppMessageReadStatus.UserID.Eq(message.ReceiverID)). + First() + + if existingStatus == nil { + // 如果不存在,创建新的已读状态记录 + now := time.Now() + _ = h.writeDB.AppMessageReadStatus.WithContext(ctx.RequestContext()).Create(&model.AppMessageReadStatus{ + AppID: req.AppID, + MessageID: message.ID, + UserID: message.ReceiverID, + IsRead: 1, + ReadTime: &now, + CreatedAt: now, + UpdatedAt: now, + }) + } else if existingStatus.IsRead == 0 { + // 如果存在但未读,更新为已读 + now := time.Now() + _, _ = h.writeDB.AppMessageReadStatus.WithContext(ctx.RequestContext()). + Where(h.writeDB.AppMessageReadStatus.ID.Eq(existingStatus.ID)). + Updates(map[string]interface{}{ + "is_read": 1, + "read_time": &now, + "updated_at": now, + }) + } + } + + res.Page = req.Page + res.PageSize = req.PageSize + res.Total = total + res.List = make([]latestMessageData, len(resultData)) + + for k, v := range resultData { + // 查询该消息的已读状态 + readStatus, _ := h.readDB.AppMessageReadStatus.WithContext(ctx.RequestContext()). + Where(h.readDB.AppMessageReadStatus.AppID.Eq(req.AppID)). + Where(h.readDB.AppMessageReadStatus.MessageID.Eq(v.ID)). + Where(h.readDB.AppMessageReadStatus.UserID.Eq(v.ReceiverID)). + First() + + // 判断是否已读 + isRead := int32(0) + if readStatus != nil && readStatus.IsRead == 1 { + isRead = 1 + } + + // 计算该用户在该应用下的未读消息总数 + unreadCount, _ := h.readDB.AppMessageReadStatus.WithContext(ctx.RequestContext()). + Where(h.readDB.AppMessageReadStatus.AppID.Eq(req.AppID)). + Where(h.readDB.AppMessageReadStatus.UserID.Eq(v.ReceiverID)). + Where(h.readDB.AppMessageReadStatus.IsRead.Eq(0)). + Count() + + res.List[k] = latestMessageData{ + MessageID: v.ID, + SendTime: timeutil.FriendlyTime(v.SendTime), + SenderID: v.SenderID, + SenderName: v.SenderName, + SenderAvatar: "", // TODO: 需要从用户表获取头像信息 + ReceiverID: v.ReceiverID, + Content: v.Content, + MsgType: v.MsgType, + IsRead: isRead, + UnreadCount: unreadCount, + } + } + + ctx.Payload(res) + } +} diff --git a/internal/api/message/message_mark_read.go b/internal/api/message/message_mark_read.go new file mode 100644 index 0000000..61e3cbb --- /dev/null +++ b/internal/api/message/message_mark_read.go @@ -0,0 +1,115 @@ +package message + +import ( + "fmt" + "net/http" + "time" + + "mini-chat/internal/code" + "mini-chat/internal/pkg/core" + "mini-chat/internal/pkg/validation" + "mini-chat/internal/repository/mysql/model" +) + +type markMessageReadRequest struct { + AppID string `json:"app_id" binding:"required"` // 小程序ID + UserID string `json:"user_id" binding:"required"` // 用户ID + MessageID int32 `json:"message_id" binding:"required"` // 消息ID +} + +type markMessageReadResponse struct { + Message string `json:"message"` +} + +// MarkMessageRead 标记消息为已读 +// @Summary 标记消息为已读 +// @Description 标记指定消息为已读状态 +// @Tags 用户端 +// @Accept json +// @Produce json +// @Param RequestBody body markMessageReadRequest true "请求参数" +// @Success 200 {object} markMessageReadResponse +// @Failure 400 {object} code.Failure +// @Router /app/messages/read [post] +func (h *handler) MarkMessageRead() core.HandlerFunc { + return func(ctx core.Context) { + req := new(markMessageReadRequest) + res := new(markMessageReadResponse) + if err := ctx.ShouldBindJSON(req); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + validation.Error(err)), + ) + return + } + + // 检查消息是否存在 + message, err := h.readDB.AppMessageLog.WithContext(ctx.RequestContext()). + Where(h.readDB.AppMessageLog.ID.Eq(req.MessageID)). + Where(h.readDB.AppMessageLog.AppID.Eq(req.AppID)). + First() + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListMessageError, + fmt.Sprintf("消息不存在:%s", err.Error())), + ) + return + } + + // 检查用户是否有权限标记此消息为已读(只有接收者可以标记) + if message.ReceiverID != req.UserID { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListMessageError, + "只有消息接收者可以标记消息为已读"), + ) + return + } + + // 检查是否已经标记为已读 + existingReadStatus, _ := h.readDB.AppMessageReadStatus.WithContext(ctx.RequestContext()). + Where(h.readDB.AppMessageReadStatus.AppID.Eq(req.AppID)). + Where(h.readDB.AppMessageReadStatus.MessageID.Eq(req.MessageID)). + Where(h.readDB.AppMessageReadStatus.UserID.Eq(req.UserID)). + First() + + now := time.Now() + + if existingReadStatus != nil { + // 如果已存在记录,更新为已读状态 + _, err = h.writeDB.AppMessageReadStatus.WithContext(ctx.RequestContext()). + Where(h.writeDB.AppMessageReadStatus.ID.Eq(existingReadStatus.ID)). + Updates(map[string]interface{}{ + "is_read": 1, + "read_time": &now, + "updated_at": now, + }) + } else { + // 如果不存在记录,创建新的已读状态记录 + newReadStatus := &model.AppMessageReadStatus{ + AppID: req.AppID, + MessageID: req.MessageID, + UserID: req.UserID, + IsRead: 1, + ReadTime: &now, + CreatedAt: now, + UpdatedAt: now, + } + err = h.writeDB.AppMessageReadStatus.WithContext(ctx.RequestContext()).Create(newReadStatus) + } + + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListMessageError, + fmt.Sprintf("标记消息已读失败:%s", err.Error())), + ) + return + } + + res.Message = "消息已标记为已读" + ctx.Payload(res) + } +} \ No newline at end of file diff --git a/internal/api/wechat/miniprogram_login.go b/internal/api/wechat/miniprogram_login.go new file mode 100644 index 0000000..9d09f2c --- /dev/null +++ b/internal/api/wechat/miniprogram_login.go @@ -0,0 +1,136 @@ +package wechat + +import ( + "encoding/json" + "fmt" + "net/http" + + "mini-chat/internal/code" + "mini-chat/internal/pkg/core" + "mini-chat/internal/pkg/httpclient" + "mini-chat/internal/pkg/validation" +) + +type miniprogramLoginRequest struct { + AppID string `json:"app_id" binding:"required"` // 小程序AppID + JSCode string `json:"js_code" binding:"required"` // 登录时获取的code +} + +type miniprogramLoginResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + OpenID string `json:"openid,omitempty"` // 用户唯一标识 + SessionKey string `json:"session_key,omitempty"` // 会话密钥 + UnionID string `json:"unionid,omitempty"` // 用户在开放平台的唯一标识符 +} + +// Code2SessionResponse 微信code2Session接口响应 +type Code2SessionResponse struct { + OpenID string `json:"openid"` // 用户唯一标识 + SessionKey string `json:"session_key"` // 会话密钥 + UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符 + ErrCode int `json:"errcode"` // 错误码 + ErrMsg string `json:"errmsg"` // 错误信息 +} + +// MiniprogramLogin 小程序登录 +// @Summary 小程序登录 +// @Description 通过AppID和code获取用户的openid和session_key +// @Tags 微信 +// @Accept json +// @Produce json +// @Param request body miniprogramLoginRequest true "请求参数" +// @Success 200 {object} miniprogramLoginResponse +// @Failure 400 {object} code.Failure +// @Failure 500 {object} code.Failure +// @Router /api/wechat/miniprogram/login [post] +func (h *handler) MiniprogramLogin() core.HandlerFunc { + return func(ctx core.Context) { + req := new(miniprogramLoginRequest) + res := new(miniprogramLoginResponse) + + if err := ctx.ShouldBindJSON(req); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + validation.Error(err), + )) + return + } + + // 根据AppID查询小程序信息获取AppSecret + miniProgram, err := h.readDB.MiniProgram.WithContext(ctx.RequestContext()). + Where(h.readDB.MiniProgram.AppID.Eq(req.AppID)). + First() + if err != nil { + h.logger.Error(fmt.Sprintf("查询小程序信息失败: %s", err.Error())) + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ServerError, + "小程序不存在或查询失败", + )) + return + } + + // 调用微信code2Session接口 + openID, sessionKey, unionID, err := h.callCode2Session(ctx, req.AppID, miniProgram.AppSecret, req.JSCode) + if err != nil { + h.logger.Error(fmt.Sprintf("调用微信code2Session接口失败: %s", err.Error())) + ctx.AbortWithError(core.Error( + http.StatusInternalServerError, + code.ServerError, + err.Error(), + )) + return + } + + res.Success = true + res.Message = "登录成功" + res.OpenID = openID + res.SessionKey = sessionKey + res.UnionID = unionID + + ctx.Payload(res) + } +} + +// callCode2Session 调用微信code2Session接口 +func (h *handler) callCode2Session(ctx core.Context, appID, appSecret, jsCode string) (string, string, string, error) { + // 构建请求URL + url := "https://api.weixin.qq.com/sns/jscode2session" + + // 发送HTTP请求 + client := httpclient.GetHttpClientWithContext(ctx.RequestContext()) + resp, err := client.R(). + SetQueryParams(map[string]string{ + "appid": appID, + "secret": appSecret, + "js_code": jsCode, + "grant_type": "authorization_code", + }). + Get(url) + + if err != nil { + return "", "", "", fmt.Errorf("HTTP请求失败: %v", err) + } + + if resp.StatusCode() != http.StatusOK { + return "", "", "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode()) + } + + var result Code2SessionResponse + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return "", "", "", fmt.Errorf("解析响应失败: %v", err) + } + + // 检查微信API返回的错误码 + if result.ErrCode != 0 { + return "", "", "", fmt.Errorf("微信API错误: errcode=%d, errmsg=%s", result.ErrCode, result.ErrMsg) + } + + if result.OpenID == "" { + return "", "", "", fmt.Errorf("获取到的openid为空") + } + + return result.OpenID, result.SessionKey, result.UnionID, nil +} diff --git a/internal/repository/mysql/dao/app_message_read_status.gen.go b/internal/repository/mysql/dao/app_message_read_status.gen.go new file mode 100644 index 0000000..8655ec1 --- /dev/null +++ b/internal/repository/mysql/dao/app_message_read_status.gen.go @@ -0,0 +1,398 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package dao + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "mini-chat/internal/repository/mysql/model" +) + +func newAppMessageReadStatus(db *gorm.DB, opts ...gen.DOOption) appMessageReadStatus { + _appMessageReadStatus := appMessageReadStatus{} + + _appMessageReadStatus.appMessageReadStatusDo.UseDB(db, opts...) + _appMessageReadStatus.appMessageReadStatusDo.UseModel(&model.AppMessageReadStatus{}) + + tableName := _appMessageReadStatus.appMessageReadStatusDo.TableName() + _appMessageReadStatus.ALL = field.NewAsterisk(tableName) + _appMessageReadStatus.ID = field.NewInt32(tableName, "id") + _appMessageReadStatus.AppID = field.NewString(tableName, "app_id") + _appMessageReadStatus.MessageID = field.NewInt32(tableName, "message_id") + _appMessageReadStatus.UserID = field.NewString(tableName, "user_id") + _appMessageReadStatus.IsRead = field.NewInt32(tableName, "is_read") + _appMessageReadStatus.ReadTime = field.NewTime(tableName, "read_time") + _appMessageReadStatus.CreatedAt = field.NewTime(tableName, "created_at") + _appMessageReadStatus.UpdatedAt = field.NewTime(tableName, "updated_at") + + _appMessageReadStatus.fillFieldMap() + + return _appMessageReadStatus +} + +// appMessageReadStatus 消息已读状态表 +type appMessageReadStatus struct { + appMessageReadStatusDo + + ALL field.Asterisk + ID field.Int32 // 主键ID + AppID field.String // 小程序ID + MessageID field.Int32 // 消息ID + UserID field.String // 用户ID + IsRead field.Int32 // 是否已读(0:未读 1:已读) + ReadTime field.Time // 已读时间 + CreatedAt field.Time // 创建时间 + UpdatedAt field.Time // 更新时间 + + fieldMap map[string]field.Expr +} + +func (a appMessageReadStatus) Table(newTableName string) *appMessageReadStatus { + a.appMessageReadStatusDo.UseTable(newTableName) + return a.updateTableName(newTableName) +} + +func (a appMessageReadStatus) As(alias string) *appMessageReadStatus { + a.appMessageReadStatusDo.DO = *(a.appMessageReadStatusDo.As(alias).(*gen.DO)) + return a.updateTableName(alias) +} + +func (a appMessageReadStatus) updateTableName(table string) *appMessageReadStatus { + a.ALL = field.NewAsterisk(table) + a.ID = field.NewInt32(table, "id") + a.AppID = field.NewString(table, "app_id") + a.MessageID = field.NewInt32(table, "message_id") + a.UserID = field.NewString(table, "user_id") + a.IsRead = field.NewInt32(table, "is_read") + a.ReadTime = field.NewTime(table, "read_time") + a.CreatedAt = field.NewTime(table, "created_at") + a.UpdatedAt = field.NewTime(table, "updated_at") + + a.fillFieldMap() + + return &a +} + +func (a *appMessageReadStatus) fillFieldMap() { + a.fieldMap = make(map[string]field.Expr, 8) + a.fieldMap["id"] = a.ID + a.fieldMap["app_id"] = a.AppID + a.fieldMap["message_id"] = a.MessageID + a.fieldMap["user_id"] = a.UserID + a.fieldMap["is_read"] = a.IsRead + a.fieldMap["read_time"] = a.ReadTime + a.fieldMap["created_at"] = a.CreatedAt + a.fieldMap["updated_at"] = a.UpdatedAt +} + +func (a appMessageReadStatus) clone(db *gorm.DB) appMessageReadStatus { + a.appMessageReadStatusDo.ReplaceConnPool(db.Statement.ConnPool) + return a +} + +func (a appMessageReadStatus) replaceDB(db *gorm.DB) appMessageReadStatus { + a.appMessageReadStatusDo.ReplaceDB(db) + return a +} + +type appMessageReadStatusDo struct{ gen.DO } + +type IAppMessageReadStatusDo interface { + gen.SubQuery + Debug() IAppMessageReadStatusDo + WithContext(ctx context.Context) IAppMessageReadStatusDo + WithResult(fc func(tx gen.Dao)) gen.ResultInfo + ReplaceDB(db *gorm.DB) + ReadDB() IAppMessageReadStatusDo + WriteDB() IAppMessageReadStatusDo + As(alias string) gen.Dao + Session(config *gorm.Session) IAppMessageReadStatusDo + Columns(cols ...field.Expr) gen.Columns + Clauses(conds ...clause.Expression) IAppMessageReadStatusDo + Not(conds ...gen.Condition) IAppMessageReadStatusDo + Or(conds ...gen.Condition) IAppMessageReadStatusDo + Select(conds ...field.Expr) IAppMessageReadStatusDo + Where(conds ...gen.Condition) IAppMessageReadStatusDo + Order(conds ...field.Expr) IAppMessageReadStatusDo + Distinct(cols ...field.Expr) IAppMessageReadStatusDo + Omit(cols ...field.Expr) IAppMessageReadStatusDo + Join(table schema.Tabler, on ...field.Expr) IAppMessageReadStatusDo + LeftJoin(table schema.Tabler, on ...field.Expr) IAppMessageReadStatusDo + RightJoin(table schema.Tabler, on ...field.Expr) IAppMessageReadStatusDo + Group(cols ...field.Expr) IAppMessageReadStatusDo + Having(conds ...gen.Condition) IAppMessageReadStatusDo + Limit(limit int) IAppMessageReadStatusDo + Offset(offset int) IAppMessageReadStatusDo + Count() (count int64, err error) + Scopes(funcs ...func(gen.Dao) gen.Dao) IAppMessageReadStatusDo + Unscoped() IAppMessageReadStatusDo + Create(values ...*model.AppMessageReadStatus) error + CreateInBatches(values []*model.AppMessageReadStatus, batchSize int) error + Save(values ...*model.AppMessageReadStatus) error + First() (*model.AppMessageReadStatus, error) + Take() (*model.AppMessageReadStatus, error) + Last() (*model.AppMessageReadStatus, error) + Find() ([]*model.AppMessageReadStatus, error) + FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.AppMessageReadStatus, err error) + FindInBatches(result *[]*model.AppMessageReadStatus, batchSize int, fc func(tx gen.Dao, batch int) error) error + Pluck(column field.Expr, dest interface{}) error + Delete(...*model.AppMessageReadStatus) (info gen.ResultInfo, err error) + Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error) + UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error) + Updates(value interface{}) (info gen.ResultInfo, err error) + UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error) + UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error) + UpdateColumns(value interface{}) (info gen.ResultInfo, err error) + UpdateFrom(q gen.SubQuery) gen.Dao + Attrs(attrs ...field.AssignExpr) IAppMessageReadStatusDo + Assign(attrs ...field.AssignExpr) IAppMessageReadStatusDo + Joins(fields ...field.RelationField) IAppMessageReadStatusDo + Preload(fields ...field.RelationField) IAppMessageReadStatusDo + FirstOrInit() (*model.AppMessageReadStatus, error) + FirstOrCreate() (*model.AppMessageReadStatus, error) + FindByPage(offset int, limit int) (result []*model.AppMessageReadStatus, count int64, err error) + ScanByPage(result interface{}, offset int, limit int) (count int64, err error) + Scan(result interface{}) (err error) + Returning(value interface{}, columns ...string) IAppMessageReadStatusDo + UnderlyingDB() *gorm.DB + schema.Tabler +} + +func (a appMessageReadStatusDo) Debug() IAppMessageReadStatusDo { + return a.withDO(a.DO.Debug()) +} + +func (a appMessageReadStatusDo) WithContext(ctx context.Context) IAppMessageReadStatusDo { + return a.withDO(a.DO.WithContext(ctx)) +} + +func (a appMessageReadStatusDo) ReadDB() IAppMessageReadStatusDo { + return a.withDO(a.DO.Clauses(dbresolver.Read)) +} + +func (a appMessageReadStatusDo) WriteDB() IAppMessageReadStatusDo { + return a.withDO(a.DO.Clauses(dbresolver.Write)) +} + +func (a appMessageReadStatusDo) Session(config *gorm.Session) IAppMessageReadStatusDo { + return a.withDO(a.DO.Session(config)) +} + +func (a appMessageReadStatusDo) Clauses(conds ...clause.Expression) IAppMessageReadStatusDo { + return a.withDO(a.DO.Clauses(conds...)) +} + +func (a appMessageReadStatusDo) Returning(value interface{}, columns ...string) IAppMessageReadStatusDo { + return a.withDO(a.DO.Returning(value, columns...)) +} + +func (a appMessageReadStatusDo) Not(conds ...gen.Condition) IAppMessageReadStatusDo { + return a.withDO(a.DO.Not(conds...)) +} + +func (a appMessageReadStatusDo) Or(conds ...gen.Condition) IAppMessageReadStatusDo { + return a.withDO(a.DO.Or(conds...)) +} + +func (a appMessageReadStatusDo) Select(conds ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Select(conds...)) +} + +func (a appMessageReadStatusDo) Where(conds ...gen.Condition) IAppMessageReadStatusDo { + return a.withDO(a.DO.Where(conds...)) +} + +func (a appMessageReadStatusDo) Order(conds ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Order(conds...)) +} + +func (a appMessageReadStatusDo) Distinct(cols ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Distinct(cols...)) +} + +func (a appMessageReadStatusDo) Omit(cols ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Omit(cols...)) +} + +func (a appMessageReadStatusDo) Join(table schema.Tabler, on ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Join(table, on...)) +} + +func (a appMessageReadStatusDo) LeftJoin(table schema.Tabler, on ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.LeftJoin(table, on...)) +} + +func (a appMessageReadStatusDo) RightJoin(table schema.Tabler, on ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.RightJoin(table, on...)) +} + +func (a appMessageReadStatusDo) Group(cols ...field.Expr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Group(cols...)) +} + +func (a appMessageReadStatusDo) Having(conds ...gen.Condition) IAppMessageReadStatusDo { + return a.withDO(a.DO.Having(conds...)) +} + +func (a appMessageReadStatusDo) Limit(limit int) IAppMessageReadStatusDo { + return a.withDO(a.DO.Limit(limit)) +} + +func (a appMessageReadStatusDo) Offset(offset int) IAppMessageReadStatusDo { + return a.withDO(a.DO.Offset(offset)) +} + +func (a appMessageReadStatusDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IAppMessageReadStatusDo { + return a.withDO(a.DO.Scopes(funcs...)) +} + +func (a appMessageReadStatusDo) Unscoped() IAppMessageReadStatusDo { + return a.withDO(a.DO.Unscoped()) +} + +func (a appMessageReadStatusDo) Create(values ...*model.AppMessageReadStatus) error { + if len(values) == 0 { + return nil + } + return a.DO.Create(values) +} + +func (a appMessageReadStatusDo) CreateInBatches(values []*model.AppMessageReadStatus, batchSize int) error { + return a.DO.CreateInBatches(values, batchSize) +} + +func (a appMessageReadStatusDo) Save(values ...*model.AppMessageReadStatus) error { + if len(values) == 0 { + return nil + } + return a.DO.Save(values) +} + +func (a appMessageReadStatusDo) First() (*model.AppMessageReadStatus, error) { + if result, err := a.DO.First(); err != nil { + return nil, err + } else { + return result.(*model.AppMessageReadStatus), nil + } +} + +func (a appMessageReadStatusDo) Take() (*model.AppMessageReadStatus, error) { + if result, err := a.DO.Take(); err != nil { + return nil, err + } else { + return result.(*model.AppMessageReadStatus), nil + } +} + +func (a appMessageReadStatusDo) Last() (*model.AppMessageReadStatus, error) { + if result, err := a.DO.Last(); err != nil { + return nil, err + } else { + return result.(*model.AppMessageReadStatus), nil + } +} + +func (a appMessageReadStatusDo) Find() ([]*model.AppMessageReadStatus, error) { + result, err := a.DO.Find() + return result.([]*model.AppMessageReadStatus), err +} + +func (a appMessageReadStatusDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.AppMessageReadStatus, err error) { + buf := make([]*model.AppMessageReadStatus, 0, batchSize) + err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (a appMessageReadStatusDo) FindInBatches(result *[]*model.AppMessageReadStatus, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return a.DO.FindInBatches(result, batchSize, fc) +} + +func (a appMessageReadStatusDo) Attrs(attrs ...field.AssignExpr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Attrs(attrs...)) +} + +func (a appMessageReadStatusDo) Assign(attrs ...field.AssignExpr) IAppMessageReadStatusDo { + return a.withDO(a.DO.Assign(attrs...)) +} + +func (a appMessageReadStatusDo) Joins(fields ...field.RelationField) IAppMessageReadStatusDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Joins(_f)) + } + return &a +} + +func (a appMessageReadStatusDo) Preload(fields ...field.RelationField) IAppMessageReadStatusDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Preload(_f)) + } + return &a +} + +func (a appMessageReadStatusDo) FirstOrInit() (*model.AppMessageReadStatus, error) { + if result, err := a.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*model.AppMessageReadStatus), nil + } +} + +func (a appMessageReadStatusDo) FirstOrCreate() (*model.AppMessageReadStatus, error) { + if result, err := a.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*model.AppMessageReadStatus), nil + } +} + +func (a appMessageReadStatusDo) FindByPage(offset int, limit int) (result []*model.AppMessageReadStatus, count int64, err error) { + result, err = a.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = a.Offset(-1).Limit(-1).Count() + return +} + +func (a appMessageReadStatusDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = a.Count() + if err != nil { + return + } + + err = a.Offset(offset).Limit(limit).Scan(result) + return +} + +func (a appMessageReadStatusDo) Scan(result interface{}) (err error) { + return a.DO.Scan(result) +} + +func (a appMessageReadStatusDo) Delete(models ...*model.AppMessageReadStatus) (result gen.ResultInfo, err error) { + return a.DO.Delete(models) +} + +func (a *appMessageReadStatusDo) withDO(do gen.Dao) *appMessageReadStatusDo { + a.DO = *do.(*gen.DO) + return a +} \ No newline at end of file diff --git a/internal/repository/mysql/dao/gen.go b/internal/repository/mysql/dao/gen.go index 0786798..841b333 100644 --- a/internal/repository/mysql/dao/gen.go +++ b/internal/repository/mysql/dao/gen.go @@ -16,15 +16,16 @@ import ( ) var ( - Q = new(Query) - Admin *admin - AppKeyword *appKeyword - AppKeywordReply *appKeywordReply - AppMessageLog *appMessageLog - AppUser *appUser - LogOperation *logOperation - LogRequest *logRequest - MiniProgram *miniProgram + Q = new(Query) + Admin *admin + AppKeyword *appKeyword + AppKeywordReply *appKeywordReply + AppMessageLog *appMessageLog + AppMessageReadStatus *appMessageReadStatus + AppUser *appUser + LogOperation *logOperation + LogRequest *logRequest + MiniProgram *miniProgram ) func SetDefault(db *gorm.DB, opts ...gen.DOOption) { @@ -33,6 +34,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { AppKeyword = &Q.AppKeyword AppKeywordReply = &Q.AppKeywordReply AppMessageLog = &Q.AppMessageLog + AppMessageReadStatus = &Q.AppMessageReadStatus AppUser = &Q.AppUser LogOperation = &Q.LogOperation LogRequest = &Q.LogRequest @@ -41,44 +43,47 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { func Use(db *gorm.DB, opts ...gen.DOOption) *Query { return &Query{ - db: db, - Admin: newAdmin(db, opts...), - AppKeyword: newAppKeyword(db, opts...), - AppKeywordReply: newAppKeywordReply(db, opts...), - AppMessageLog: newAppMessageLog(db, opts...), - AppUser: newAppUser(db, opts...), - LogOperation: newLogOperation(db, opts...), - LogRequest: newLogRequest(db, opts...), - MiniProgram: newMiniProgram(db, opts...), + db: db, + Admin: newAdmin(db, opts...), + AppKeyword: newAppKeyword(db, opts...), + AppKeywordReply: newAppKeywordReply(db, opts...), + AppMessageLog: newAppMessageLog(db, opts...), + AppMessageReadStatus: newAppMessageReadStatus(db, opts...), + AppUser: newAppUser(db, opts...), + LogOperation: newLogOperation(db, opts...), + LogRequest: newLogRequest(db, opts...), + MiniProgram: newMiniProgram(db, opts...), } } type Query struct { db *gorm.DB - Admin admin - AppKeyword appKeyword - AppKeywordReply appKeywordReply - AppMessageLog appMessageLog - AppUser appUser - LogOperation logOperation - LogRequest logRequest - MiniProgram miniProgram + Admin admin + AppKeyword appKeyword + AppKeywordReply appKeywordReply + AppMessageLog appMessageLog + AppMessageReadStatus appMessageReadStatus + AppUser appUser + LogOperation logOperation + LogRequest logRequest + MiniProgram miniProgram } func (q *Query) Available() bool { return q.db != nil } func (q *Query) clone(db *gorm.DB) *Query { return &Query{ - db: db, - Admin: q.Admin.clone(db), - AppKeyword: q.AppKeyword.clone(db), - AppKeywordReply: q.AppKeywordReply.clone(db), - AppMessageLog: q.AppMessageLog.clone(db), - AppUser: q.AppUser.clone(db), - LogOperation: q.LogOperation.clone(db), - LogRequest: q.LogRequest.clone(db), - MiniProgram: q.MiniProgram.clone(db), + db: db, + Admin: q.Admin.clone(db), + AppKeyword: q.AppKeyword.clone(db), + AppKeywordReply: q.AppKeywordReply.clone(db), + AppMessageLog: q.AppMessageLog.clone(db), + AppMessageReadStatus: q.AppMessageReadStatus.clone(db), + AppUser: q.AppUser.clone(db), + LogOperation: q.LogOperation.clone(db), + LogRequest: q.LogRequest.clone(db), + MiniProgram: q.MiniProgram.clone(db), } } @@ -92,39 +97,42 @@ func (q *Query) WriteDB() *Query { func (q *Query) ReplaceDB(db *gorm.DB) *Query { return &Query{ - db: db, - Admin: q.Admin.replaceDB(db), - AppKeyword: q.AppKeyword.replaceDB(db), - AppKeywordReply: q.AppKeywordReply.replaceDB(db), - AppMessageLog: q.AppMessageLog.replaceDB(db), - AppUser: q.AppUser.replaceDB(db), - LogOperation: q.LogOperation.replaceDB(db), - LogRequest: q.LogRequest.replaceDB(db), - MiniProgram: q.MiniProgram.replaceDB(db), + db: db, + Admin: q.Admin.replaceDB(db), + AppKeyword: q.AppKeyword.replaceDB(db), + AppKeywordReply: q.AppKeywordReply.replaceDB(db), + AppMessageLog: q.AppMessageLog.replaceDB(db), + AppMessageReadStatus: q.AppMessageReadStatus.replaceDB(db), + AppUser: q.AppUser.replaceDB(db), + LogOperation: q.LogOperation.replaceDB(db), + LogRequest: q.LogRequest.replaceDB(db), + MiniProgram: q.MiniProgram.replaceDB(db), } } type queryCtx struct { - Admin *adminDo - AppKeyword *appKeywordDo - AppKeywordReply *appKeywordReplyDo - AppMessageLog *appMessageLogDo - AppUser *appUserDo - LogOperation *logOperationDo - LogRequest *logRequestDo - MiniProgram *miniProgramDo + Admin *adminDo + AppKeyword *appKeywordDo + AppKeywordReply *appKeywordReplyDo + AppMessageLog *appMessageLogDo + AppMessageReadStatus *appMessageReadStatusDo + AppUser *appUserDo + LogOperation *logOperationDo + LogRequest *logRequestDo + MiniProgram *miniProgramDo } func (q *Query) WithContext(ctx context.Context) *queryCtx { return &queryCtx{ - Admin: q.Admin.WithContext(ctx), - AppKeyword: q.AppKeyword.WithContext(ctx), - AppKeywordReply: q.AppKeywordReply.WithContext(ctx), - AppMessageLog: q.AppMessageLog.WithContext(ctx), - AppUser: q.AppUser.WithContext(ctx), - LogOperation: q.LogOperation.WithContext(ctx), - LogRequest: q.LogRequest.WithContext(ctx), - MiniProgram: q.MiniProgram.WithContext(ctx), + Admin: q.Admin.WithContext(ctx), + AppKeyword: q.AppKeyword.WithContext(ctx), + AppKeywordReply: q.AppKeywordReply.WithContext(ctx), + AppMessageLog: q.AppMessageLog.WithContext(ctx), + AppMessageReadStatus: q.AppMessageReadStatus.WithContext(ctx).(*appMessageReadStatusDo), + AppUser: q.AppUser.WithContext(ctx), + LogOperation: q.LogOperation.WithContext(ctx), + LogRequest: q.LogRequest.WithContext(ctx), + MiniProgram: q.MiniProgram.WithContext(ctx), } } diff --git a/internal/repository/mysql/model/app_message_read_status.gen.go b/internal/repository/mysql/model/app_message_read_status.gen.go new file mode 100644 index 0000000..ff6cd21 --- /dev/null +++ b/internal/repository/mysql/model/app_message_read_status.gen.go @@ -0,0 +1,28 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameAppMessageReadStatus = "app_message_read_status" + +// AppMessageReadStatus 消息已读状态表 +type AppMessageReadStatus struct { + ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID + AppID string `gorm:"column:app_id;not null;comment:小程序ID" json:"app_id"` // 小程序ID + MessageID int32 `gorm:"column:message_id;not null;comment:消息ID" json:"message_id"` // 消息ID + UserID string `gorm:"column:user_id;not null;comment:用户ID" json:"user_id"` // 用户ID + IsRead int32 `gorm:"column:is_read;not null;default:0;comment:是否已读(0:未读 1:已读)" json:"is_read"` // 是否已读(0:未读 1:已读) + ReadTime *time.Time `gorm:"column:read_time;comment:已读时间" json:"read_time"` // 已读时间 + CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间 +} + +// TableName AppMessageReadStatus's table name +func (*AppMessageReadStatus) TableName() string { + return TableNameAppMessageReadStatus +} \ No newline at end of file diff --git a/internal/router/router.go b/internal/router/router.go index 97020f2..a6f3d1c 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -63,15 +63,17 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co appNonAuthApiRouter := mux.Group("/app") { - appNonAuthApiRouter.POST("/user/create", appHandler.CreateAppUser()) // 新增小程序用户 - appNonAuthApiRouter.GET("/messages", messageHandler.AppMessagePageList()) // 消息列表 - appNonAuthApiRouter.POST("/send_message", messageHandler.UserSendMessage()) // 发送消息 + appNonAuthApiRouter.POST("/user/create", appHandler.CreateAppUser()) // 新增小程序用户 + appNonAuthApiRouter.GET("/messages", messageHandler.AppMessagePageList()) // 消息列表 + appNonAuthApiRouter.POST("/messages/read", messageHandler.MarkMessageRead()) // 标记消息为已读 + appNonAuthApiRouter.POST("/send_message", messageHandler.UserSendMessage()) // 发送消息 } // 微信 API 路由组 wechatApiRouter := mux.Group("/api/wechat") { - wechatApiRouter.POST("/qrcode", wechatHandler.GenerateQRCode()) // 生成微信小程序二维码(返回 Base64) + wechatApiRouter.POST("/qrcode", wechatHandler.GenerateQRCode()) // 生成微信小程序二维码(返回 Base64) + wechatApiRouter.POST("/miniprogram/login", wechatHandler.MiniprogramLogin()) // 小程序登录 } // 管理端认证接口路由组 @@ -99,6 +101,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co adminAuthApiRouter.GET("/app/users", appHandler.UserPageList()) // 获取小程序用户列表 adminAuthApiRouter.POST("/send_message", appHandler.AdminSendMessage()) // 发送消息 adminAuthApiRouter.GET("/messages", appHandler.AppMessagePageList()) // 获取小程序用户消息列表 + adminAuthApiRouter.GET("/messages/latest", appHandler.LatestMessageByAppId()) // 根据appid获取最新消息记录 } return mux, nil