feat(消息状态): 添加消息已读状态功能

- 新增消息已读状态表结构及模型
- 实现用户端标记消息为已读接口
- 添加管理端获取最新消息记录接口
- 实现微信小程序登录功能
- 更新相关API文档和路由配置
This commit is contained in:
邹方成 2025-10-18 18:24:42 +08:00
parent f1a364bae2
commit 1a285f4e23
10 changed files with 1688 additions and 64 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -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:

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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