diff --git a/docs/docs.go b/docs/docs.go index 64e74db..d1789a3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1165,7 +1165,7 @@ const docTemplate = `{ }, "/api/wechat/miniprogram/login": { "post": { - "description": "通过AppID和code获取用户的openid和session_key", + "description": "通过AppID和code获取用户的openid,系统自动生成用户名和头像", "consumes": [ "application/json" ], @@ -2378,49 +2378,6 @@ const docTemplate = `{ } } }, - "wechat.DecryptedUserInfo": { - "type": "object", - "properties": { - "avatarUrl": { - "type": "string" - }, - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "gender": { - "type": "integer" - }, - "nickName": { - "type": "string" - }, - "openId": { - "type": "string" - }, - "province": { - "type": "string" - }, - "unionId": { - "type": "string" - }, - "watermark": { - "$ref": "#/definitions/wechat.Watermark" - } - } - }, - "wechat.Watermark": { - "type": "object", - "properties": { - "appid": { - "type": "string" - }, - "timestamp": { - "type": "integer" - } - } - }, "wechat.generateQRCodeRequest": { "type": "object", "required": [ @@ -2469,39 +2426,15 @@ const docTemplate = `{ "description": "小程序AppID", "type": "string" }, - "encrypted_data": { - "description": "加密数据(可选)", - "type": "string" - }, - "iv": { - "description": "初始向量(可选)", - "type": "string" - }, "js_code": { "description": "登录时获取的code", "type": "string" - }, - "raw_data": { - "description": "原始数据(可选,用于签名验证)", - "type": "string" - }, - "signature": { - "description": "签名(可选,用于验证数据完整性)", - "type": "string" } } }, "wechat.miniprogramLoginResponse": { "type": "object", "properties": { - "decrypted_data": { - "description": "解密后的用户信息(可选)", - "allOf": [ - { - "$ref": "#/definitions/wechat.DecryptedUserInfo" - } - ] - }, "message": { "type": "string" }, @@ -2509,16 +2442,28 @@ const docTemplate = `{ "description": "用户唯一标识", "type": "string" }, - "session_key": { - "description": "会话密钥", - "type": "string" - }, "success": { "type": "boolean" }, + "token": { + "description": "登录token", + "type": "string" + }, "unionid": { "description": "用户在开放平台的唯一标识符", "type": "string" + }, + "user_avatar": { + "description": "用户头像", + "type": "string" + }, + "user_id": { + "description": "用户ID", + "type": "string" + }, + "user_name": { + "description": "用户昵称", + "type": "string" } } } diff --git a/docs/swagger.json b/docs/swagger.json index bc21469..52a3983 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1157,7 +1157,7 @@ }, "/api/wechat/miniprogram/login": { "post": { - "description": "通过AppID和code获取用户的openid和session_key", + "description": "通过AppID和code获取用户的openid,系统自动生成用户名和头像", "consumes": [ "application/json" ], @@ -2370,49 +2370,6 @@ } } }, - "wechat.DecryptedUserInfo": { - "type": "object", - "properties": { - "avatarUrl": { - "type": "string" - }, - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "gender": { - "type": "integer" - }, - "nickName": { - "type": "string" - }, - "openId": { - "type": "string" - }, - "province": { - "type": "string" - }, - "unionId": { - "type": "string" - }, - "watermark": { - "$ref": "#/definitions/wechat.Watermark" - } - } - }, - "wechat.Watermark": { - "type": "object", - "properties": { - "appid": { - "type": "string" - }, - "timestamp": { - "type": "integer" - } - } - }, "wechat.generateQRCodeRequest": { "type": "object", "required": [ @@ -2461,39 +2418,15 @@ "description": "小程序AppID", "type": "string" }, - "encrypted_data": { - "description": "加密数据(可选)", - "type": "string" - }, - "iv": { - "description": "初始向量(可选)", - "type": "string" - }, "js_code": { "description": "登录时获取的code", "type": "string" - }, - "raw_data": { - "description": "原始数据(可选,用于签名验证)", - "type": "string" - }, - "signature": { - "description": "签名(可选,用于验证数据完整性)", - "type": "string" } } }, "wechat.miniprogramLoginResponse": { "type": "object", "properties": { - "decrypted_data": { - "description": "解密后的用户信息(可选)", - "allOf": [ - { - "$ref": "#/definitions/wechat.DecryptedUserInfo" - } - ] - }, "message": { "type": "string" }, @@ -2501,16 +2434,28 @@ "description": "用户唯一标识", "type": "string" }, - "session_key": { - "description": "会话密钥", - "type": "string" - }, "success": { "type": "boolean" }, + "token": { + "description": "登录token", + "type": "string" + }, "unionid": { "description": "用户在开放平台的唯一标识符", "type": "string" + }, + "user_avatar": { + "description": "用户头像", + "type": "string" + }, + "user_id": { + "description": "用户ID", + "type": "string" + }, + "user_name": { + "description": "用户昵称", + "type": "string" } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1cfcb38..235320a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -695,34 +695,6 @@ definitions: description: 真实图片地址 type: string type: object - wechat.DecryptedUserInfo: - properties: - avatarUrl: - type: string - city: - type: string - country: - type: string - gender: - type: integer - nickName: - type: string - openId: - type: string - province: - type: string - unionId: - type: string - watermark: - $ref: '#/definitions/wechat.Watermark' - type: object - wechat.Watermark: - properties: - appid: - type: string - timestamp: - type: integer - type: object wechat.generateQRCodeRequest: properties: app_id: @@ -754,44 +726,37 @@ definitions: app_id: description: 小程序AppID type: string - encrypted_data: - description: 加密数据(可选) - type: string - iv: - description: 初始向量(可选) - type: string js_code: description: 登录时获取的code type: string - raw_data: - description: 原始数据(可选,用于签名验证) - type: string - signature: - description: 签名(可选,用于验证数据完整性) - type: string required: - app_id - js_code type: object wechat.miniprogramLoginResponse: properties: - decrypted_data: - allOf: - - $ref: '#/definitions/wechat.DecryptedUserInfo' - description: 解密后的用户信息(可选) message: type: string openid: description: 用户唯一标识 type: string - session_key: - description: 会话密钥 - type: string success: type: boolean + token: + description: 登录token + type: string unionid: description: 用户在开放平台的唯一标识符 type: string + user_avatar: + description: 用户头像 + type: string + user_id: + description: 用户ID + type: string + user_name: + description: 用户昵称 + type: string type: object info: contact: {} @@ -1542,7 +1507,7 @@ paths: post: consumes: - application/json - description: 通过AppID和code获取用户的openid和session_key + description: 通过AppID和code获取用户的openid,系统自动生成用户名和头像 parameters: - description: 请求参数 in: body diff --git a/go.mod b/go.mod index bf4f311..3775c4b 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -79,6 +80,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/o1egl/govatar v0.4.1 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect diff --git a/go.sum b/go.sum index 5fd5a1d..e88c659 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,7 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -241,6 +242,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -318,6 +321,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/o1egl/govatar v0.4.1 h1:RRzAxm52WpZMSEoWgAXrTcXWKhIUPpgpI54KP+UI0Ew= +github.com/o1egl/govatar v0.4.1/go.mod h1:cSBJjpgYiKmQ8E+C4zNBcsbuDwy9UH4HS8BwE4m6JmQ= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= @@ -344,10 +349,12 @@ github.com/rs/cors v1.8.1 h1:OrP+y5H+5Md29ACTA9imbALaKHwOSUZkcizaG0LT5ow= github.com/rs/cors v1.8.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644 h1:BBwREPixt0iE77C9z7DOenoeh5OGFrzyL1cWOp5oQTs= github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644/go.mod h1:gmu40DuK3SLdKUzGOUofS3UDZwyeOUy6ZjPPuaALatw= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -395,6 +402,7 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -800,6 +808,7 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/api/app/app_latest_messages.go b/internal/api/app/app_latest_messages.go index 8a9e881..fd191a5 100644 --- a/internal/api/app/app_latest_messages.go +++ b/internal/api/app/app_latest_messages.go @@ -94,7 +94,7 @@ func (h *handler) LatestMessageByAppId() core.HandlerFunc { // 分页查询指定小程序的最新消息 resultData, err := query. - Order(h.readDB.AppMessageLog.SendTime.Desc()). + Order(h.readDB.AppMessageLog.SendTime.Asc()). Offset((req.Page - 1) * req.PageSize). Limit(req.PageSize). Find() diff --git a/internal/api/app/app_message_list.go b/internal/api/app/app_message_list.go index 33cfa2e..e320b23 100644 --- a/internal/api/app/app_message_list.go +++ b/internal/api/app/app_message_list.go @@ -87,7 +87,7 @@ func (h *handler) AppMessagePageList() core.HandlerFunc { countQueryDB := query.Session(&gorm.Session{}) resultData, err := listQueryDB. - Order(h.readDB.AppMessageLog.SendTime.Desc()). + Order(h.readDB.AppMessageLog.SendTime.Asc()). Limit(req.PageSize). Offset((req.Page - 1) * req.PageSize). Find() diff --git a/internal/api/message/message_list_user.go b/internal/api/message/message_list_user.go index 3d9e0d6..ab07808 100644 --- a/internal/api/message/message_list_user.go +++ b/internal/api/message/message_list_user.go @@ -86,7 +86,7 @@ func (h *handler) AppMessagePageList() core.HandlerFunc { countQueryDB := query.Session(&gorm.Session{}) resultData, err := listQueryDB. - Order(h.readDB.AppMessageLog.SendTime.Desc()). + Order(h.readDB.AppMessageLog.SendTime.Asc()). Limit(req.PageSize). Offset((req.Page - 1) * req.PageSize). Find() diff --git a/internal/api/wechat/miniprogram_login.go b/internal/api/wechat/miniprogram_login.go index 0eb7a8e..f15a05a 100644 --- a/internal/api/wechat/miniprogram_login.go +++ b/internal/api/wechat/miniprogram_login.go @@ -1,33 +1,39 @@ package wechat import ( + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" + "image/png" "net/http" + "os" + "time" + + "github.com/goombaio/namegenerator" + "github.com/o1egl/govatar" "mini-chat/internal/code" "mini-chat/internal/pkg/core" "mini-chat/internal/pkg/httpclient" "mini-chat/internal/pkg/validation" - "mini-chat/internal/pkg/wechat" + "mini-chat/internal/repository/mysql/model" ) type miniprogramLoginRequest struct { - AppID string `json:"app_id" binding:"required"` // 小程序AppID - JSCode string `json:"js_code" binding:"required"` // 登录时获取的code - EncryptedData string `json:"encrypted_data,omitempty"` // 加密数据(可选) - IV string `json:"iv,omitempty"` // 初始向量(可选) - RawData string `json:"raw_data,omitempty"` // 原始数据(可选,用于签名验证) - Signature string `json:"signature,omitempty"` // 签名(可选,用于验证数据完整性) + 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"` // 用户在开放平台的唯一标识符 - DecryptedData *wechat.DecryptedUserInfo `json:"decrypted_data,omitempty"` // 解密后的用户信息(可选) + Success bool `json:"success"` + Message string `json:"message"` + Token string `json:"token"` // 登录token + UserID string `json:"user_id"` // 用户ID + UserName string `json:"user_name"` // 用户昵称 + Avatar string `json:"user_avatar"` // 用户头像 + OpenID string `json:"openid,omitempty"` // 用户唯一标识 + UnionID string `json:"unionid,omitempty"` // 用户在开放平台的唯一标识符 } // Code2SessionResponse 微信code2Session接口响应 @@ -41,7 +47,7 @@ type Code2SessionResponse struct { // MiniprogramLogin 小程序登录 // @Summary 小程序登录 -// @Description 通过AppID和code获取用户的openid和session_key +// @Description 通过AppID和code获取用户的openid,系统自动生成用户名和头像 // @Tags 微信 // @Accept json // @Produce json @@ -90,46 +96,150 @@ func (h *handler) MiniprogramLogin() core.HandlerFunc { return } + // 查询或创建用户 + user, err := h.getOrCreateUser(ctx, req.AppID, openID, unionID) + if err != nil { + h.logger.Error(fmt.Sprintf("获取或创建用户失败: %s", err.Error())) + ctx.AbortWithError(core.Error( + http.StatusInternalServerError, + code.ServerError, + "用户创建失败", + )) + return + } + + // 生成登录token + token, err := h.generateToken(user.UserID, sessionKey) + if err != nil { + h.logger.Error(fmt.Sprintf("生成token失败: %s", err.Error())) + ctx.AbortWithError(core.Error( + http.StatusInternalServerError, + code.ServerError, + "token生成失败", + )) + return + } + res.Success = true res.Message = "登录成功" + res.Token = token + res.UserID = user.UserID + res.UserName = user.UserName + res.Avatar = user.UserAvatar res.OpenID = openID - res.SessionKey = sessionKey res.UnionID = unionID - // 如果提供了加密数据,则进行解密 - if req.EncryptedData != "" && req.IV != "" { - // 如果提供了签名验证数据,先验证签名 - if req.RawData != "" && req.Signature != "" { - if !wechat.VerifySignature(req.RawData, req.Signature, sessionKey) { - h.logger.Warn("数据签名验证失败") - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ParamBindError, - "数据签名验证失败", - )) - return - } - } - - // 解密用户数据 - decryptedUserInfo, err := wechat.DecryptUserInfo(sessionKey, req.EncryptedData, req.IV) - if err != nil { - h.logger.Error(fmt.Sprintf("解密用户数据失败: %s", err.Error())) - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - "解密用户数据失败", - )) - return - } - - res.DecryptedData = decryptedUserInfo - } - ctx.Payload(res) } } +// getOrCreateUser 获取或创建用户 +func (h *handler) getOrCreateUser(ctx core.Context, appID, openID, unionID string) (*model.AppUser, error) { + // 先查询用户是否存在(使用openID作为用户ID) + user, err := h.readDB.AppUser.WithContext(ctx.RequestContext()). + Where(h.readDB.AppUser.AppID.Eq(appID)). + Where(h.readDB.AppUser.UserID.Eq(openID)). + First() + + if err == nil { + // 用户已存在,直接返回 + return user, nil + } + + // 用户不存在,创建新用户 + // 生成随机用户名 + seed := time.Now().UTC().UnixNano() + nameGen := namegenerator.NewNameGenerator(seed) + username := nameGen.Generate() + + // 生成头像URL + avatarURL, err := h.generateAvatar(openID) + if err != nil { + h.logger.Warn(fmt.Sprintf("生成头像失败: %s,使用默认头像", err.Error())) + avatarURL = "/static/avatars/default.svg" + } + + // 创建新用户 + newUser := &model.AppUser{ + AppID: appID, + UserID: openID, // 使用openID作为用户ID + UserName: username, + UserMobile: "", // 暂时为空 + UserAvatar: avatarURL, + CreatedAt: time.Now(), + } + + err = h.writeDB.AppUser.WithContext(ctx.RequestContext()).Create(newUser) + if err != nil { + return nil, fmt.Errorf("创建用户失败: %v", err) + } + + return newUser, nil +} + +// generateAvatar 生成头像 +func (h *handler) generateAvatar(seed string) (string, error) { + // 根据seed生成随机性别 + gender := govatar.MALE + if len(seed)%2 == 0 { + gender = govatar.FEMALE + } + + // 生成头像文件名(基于seed的hash) + filename := fmt.Sprintf("%x", seed) + if len(filename) > 16 { + filename = filename[:16] + } + + // 生成头像文件路径 + avatarFilename := fmt.Sprintf("%s_%d.png", filename, gender) + avatarPath := fmt.Sprintf("static/avatars/%s", avatarFilename) + avatarURL := fmt.Sprintf("/static/avatars/%s", avatarFilename) + + // 检查文件是否已存在 + if _, err := os.Stat(avatarPath); err == nil { + // 文件已存在,直接返回URL + return avatarURL, nil + } + + // 生成头像图片 + img, err := govatar.GenerateForUsername(gender, seed) + if err != nil { + return "", fmt.Errorf("生成头像失败: %v", err) + } + + // 创建文件 + file, err := os.Create(avatarPath) + if err != nil { + return "", fmt.Errorf("创建头像文件失败: %v", err) + } + defer file.Close() + + // 保存PNG图片 + err = png.Encode(file, img) + if err != nil { + return "", fmt.Errorf("保存头像文件失败: %v", err) + } + + return avatarURL, nil +} + +// generateToken 生成登录token +func (h *handler) generateToken(userID, sessionKey string) (string, error) { + // 生成32字节的随机token + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + + token := hex.EncodeToString(bytes) + + // TODO: 这里应该将token保存到缓存或数据库中,并设置过期时间 + // 可以结合sessionKey一起存储,用于后续的用户身份验证 + + return token, nil +} + // callCode2Session 调用微信code2Session接口 func (h *handler) callCode2Session(ctx core.Context, appID, appSecret, jsCode string) (string, string, string, error) { // 构建请求URL diff --git a/internal/pkg/core/core.go b/internal/pkg/core/core.go index 7ba0b1e..5fb4f2b 100644 --- a/internal/pkg/core/core.go +++ b/internal/pkg/core/core.go @@ -233,6 +233,7 @@ func New(logger logger.CustomLogger, options ...Option) (Mux, error) { mux.engine.Use(cors.New()) mux.engine.StaticFS("resources", gin.Dir(configs.GetResourcesFilePath(), true)) + mux.engine.StaticFS("static", gin.Dir("static", true)) // withoutTracePaths 这些请求,默认不记录日志 withoutTracePaths := map[string]bool{ diff --git a/static/avatars/default.svg b/static/avatars/default.svg new file mode 100644 index 0000000..5875c4b --- /dev/null +++ b/static/avatars/default.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file