feat(wechat): 重构小程序登录接口,实现自动生成用户信息和头像

- 移除微信用户信息解密相关代码,改为系统自动生成用户名和头像
- 添加用户信息存储功能,使用openID作为用户ID
- 集成govatar和namegenerator库生成用户头像和随机用户名
- 添加token生成功能,返回给客户端用于后续认证
- 更新swagger文档,反映接口变更
This commit is contained in:
邹方成 2025-10-19 00:34:02 +08:00
parent e4d4258918
commit a4e532c6b6
11 changed files with 222 additions and 240 deletions

View File

@ -1165,7 +1165,7 @@ const docTemplate = `{
}, },
"/api/wechat/miniprogram/login": { "/api/wechat/miniprogram/login": {
"post": { "post": {
"description": "通过AppID和code获取用户的openid和session_key", "description": "通过AppID和code获取用户的openid,系统自动生成用户名和头像",
"consumes": [ "consumes": [
"application/json" "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": { "wechat.generateQRCodeRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@ -2469,39 +2426,15 @@ const docTemplate = `{
"description": "小程序AppID", "description": "小程序AppID",
"type": "string" "type": "string"
}, },
"encrypted_data": {
"description": "加密数据(可选)",
"type": "string"
},
"iv": {
"description": "初始向量(可选)",
"type": "string"
},
"js_code": { "js_code": {
"description": "登录时获取的code", "description": "登录时获取的code",
"type": "string" "type": "string"
},
"raw_data": {
"description": "原始数据(可选,用于签名验证)",
"type": "string"
},
"signature": {
"description": "签名(可选,用于验证数据完整性)",
"type": "string"
} }
} }
}, },
"wechat.miniprogramLoginResponse": { "wechat.miniprogramLoginResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"decrypted_data": {
"description": "解密后的用户信息(可选)",
"allOf": [
{
"$ref": "#/definitions/wechat.DecryptedUserInfo"
}
]
},
"message": { "message": {
"type": "string" "type": "string"
}, },
@ -2509,16 +2442,28 @@ const docTemplate = `{
"description": "用户唯一标识", "description": "用户唯一标识",
"type": "string" "type": "string"
}, },
"session_key": {
"description": "会话密钥",
"type": "string"
},
"success": { "success": {
"type": "boolean" "type": "boolean"
}, },
"token": {
"description": "登录token",
"type": "string"
},
"unionid": { "unionid": {
"description": "用户在开放平台的唯一标识符", "description": "用户在开放平台的唯一标识符",
"type": "string" "type": "string"
},
"user_avatar": {
"description": "用户头像",
"type": "string"
},
"user_id": {
"description": "用户ID",
"type": "string"
},
"user_name": {
"description": "用户昵称",
"type": "string"
} }
} }
} }

View File

@ -1157,7 +1157,7 @@
}, },
"/api/wechat/miniprogram/login": { "/api/wechat/miniprogram/login": {
"post": { "post": {
"description": "通过AppID和code获取用户的openid和session_key", "description": "通过AppID和code获取用户的openid,系统自动生成用户名和头像",
"consumes": [ "consumes": [
"application/json" "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": { "wechat.generateQRCodeRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@ -2461,39 +2418,15 @@
"description": "小程序AppID", "description": "小程序AppID",
"type": "string" "type": "string"
}, },
"encrypted_data": {
"description": "加密数据(可选)",
"type": "string"
},
"iv": {
"description": "初始向量(可选)",
"type": "string"
},
"js_code": { "js_code": {
"description": "登录时获取的code", "description": "登录时获取的code",
"type": "string" "type": "string"
},
"raw_data": {
"description": "原始数据(可选,用于签名验证)",
"type": "string"
},
"signature": {
"description": "签名(可选,用于验证数据完整性)",
"type": "string"
} }
} }
}, },
"wechat.miniprogramLoginResponse": { "wechat.miniprogramLoginResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"decrypted_data": {
"description": "解密后的用户信息(可选)",
"allOf": [
{
"$ref": "#/definitions/wechat.DecryptedUserInfo"
}
]
},
"message": { "message": {
"type": "string" "type": "string"
}, },
@ -2501,16 +2434,28 @@
"description": "用户唯一标识", "description": "用户唯一标识",
"type": "string" "type": "string"
}, },
"session_key": {
"description": "会话密钥",
"type": "string"
},
"success": { "success": {
"type": "boolean" "type": "boolean"
}, },
"token": {
"description": "登录token",
"type": "string"
},
"unionid": { "unionid": {
"description": "用户在开放平台的唯一标识符", "description": "用户在开放平台的唯一标识符",
"type": "string" "type": "string"
},
"user_avatar": {
"description": "用户头像",
"type": "string"
},
"user_id": {
"description": "用户ID",
"type": "string"
},
"user_name": {
"description": "用户昵称",
"type": "string"
} }
} }
} }

View File

@ -695,34 +695,6 @@ definitions:
description: 真实图片地址 description: 真实图片地址
type: string type: string
type: object 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: wechat.generateQRCodeRequest:
properties: properties:
app_id: app_id:
@ -754,44 +726,37 @@ definitions:
app_id: app_id:
description: 小程序AppID description: 小程序AppID
type: string type: string
encrypted_data:
description: 加密数据(可选)
type: string
iv:
description: 初始向量(可选)
type: string
js_code: js_code:
description: 登录时获取的code description: 登录时获取的code
type: string type: string
raw_data:
description: 原始数据(可选,用于签名验证)
type: string
signature:
description: 签名(可选,用于验证数据完整性)
type: string
required: required:
- app_id - app_id
- js_code - js_code
type: object type: object
wechat.miniprogramLoginResponse: wechat.miniprogramLoginResponse:
properties: properties:
decrypted_data:
allOf:
- $ref: '#/definitions/wechat.DecryptedUserInfo'
description: 解密后的用户信息(可选)
message: message:
type: string type: string
openid: openid:
description: 用户唯一标识 description: 用户唯一标识
type: string type: string
session_key:
description: 会话密钥
type: string
success: success:
type: boolean type: boolean
token:
description: 登录token
type: string
unionid: unionid:
description: 用户在开放平台的唯一标识符 description: 用户在开放平台的唯一标识符
type: string type: string
user_avatar:
description: 用户头像
type: string
user_id:
description: 用户ID
type: string
user_name:
description: 用户昵称
type: string
type: object type: object
info: info:
contact: {} contact: {}
@ -1542,7 +1507,7 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: 通过AppID和code获取用户的openid和session_key description: 通过AppID和code获取用户的openid,系统自动生成用户名和头像
parameters: parameters:
- description: 请求参数 - description: 请求参数
in: body in: body

2
go.mod
View File

@ -63,6 +63,7 @@ require (
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // 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/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
@ -79,6 +80,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/common v0.44.0 // indirect

9
go.sum
View File

@ -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/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 h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/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.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/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/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-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/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= 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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/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.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 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 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 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 h1:BBwREPixt0iE77C9z7DOenoeh5OGFrzyL1cWOp5oQTs=
github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644/go.mod h1:gmu40DuK3SLdKUzGOUofS3UDZwyeOUy6ZjPPuaALatw= 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 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= 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 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 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 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/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 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.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 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/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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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= 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 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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.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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -94,7 +94,7 @@ func (h *handler) LatestMessageByAppId() core.HandlerFunc {
// 分页查询指定小程序的最新消息 // 分页查询指定小程序的最新消息
resultData, err := query. resultData, err := query.
Order(h.readDB.AppMessageLog.SendTime.Desc()). Order(h.readDB.AppMessageLog.SendTime.Asc()).
Offset((req.Page - 1) * req.PageSize). Offset((req.Page - 1) * req.PageSize).
Limit(req.PageSize). Limit(req.PageSize).
Find() Find()

View File

@ -87,7 +87,7 @@ func (h *handler) AppMessagePageList() core.HandlerFunc {
countQueryDB := query.Session(&gorm.Session{}) countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB. resultData, err := listQueryDB.
Order(h.readDB.AppMessageLog.SendTime.Desc()). Order(h.readDB.AppMessageLog.SendTime.Asc()).
Limit(req.PageSize). Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize). Offset((req.Page - 1) * req.PageSize).
Find() Find()

View File

@ -86,7 +86,7 @@ func (h *handler) AppMessagePageList() core.HandlerFunc {
countQueryDB := query.Session(&gorm.Session{}) countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB. resultData, err := listQueryDB.
Order(h.readDB.AppMessageLog.SendTime.Desc()). Order(h.readDB.AppMessageLog.SendTime.Asc()).
Limit(req.PageSize). Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize). Offset((req.Page - 1) * req.PageSize).
Find() Find()

View File

@ -1,33 +1,39 @@
package wechat package wechat
import ( import (
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"image/png"
"net/http" "net/http"
"os"
"time"
"github.com/goombaio/namegenerator"
"github.com/o1egl/govatar"
"mini-chat/internal/code" "mini-chat/internal/code"
"mini-chat/internal/pkg/core" "mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/httpclient" "mini-chat/internal/pkg/httpclient"
"mini-chat/internal/pkg/validation" "mini-chat/internal/pkg/validation"
"mini-chat/internal/pkg/wechat" "mini-chat/internal/repository/mysql/model"
) )
type miniprogramLoginRequest struct { type miniprogramLoginRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序AppID AppID string `json:"app_id" binding:"required"` // 小程序AppID
JSCode string `json:"js_code" binding:"required"` // 登录时获取的code 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"` // 签名(可选,用于验证数据完整性)
} }
type miniprogramLoginResponse struct { type miniprogramLoginResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
OpenID string `json:"openid,omitempty"` // 用户唯一标识 Token string `json:"token"` // 登录token
SessionKey string `json:"session_key,omitempty"` // 会话密钥 UserID string `json:"user_id"` // 用户ID
UnionID string `json:"unionid,omitempty"` // 用户在开放平台的唯一标识符 UserName string `json:"user_name"` // 用户昵称
DecryptedData *wechat.DecryptedUserInfo `json:"decrypted_data,omitempty"` // 解密后的用户信息(可选) Avatar string `json:"user_avatar"` // 用户头像
OpenID string `json:"openid,omitempty"` // 用户唯一标识
UnionID string `json:"unionid,omitempty"` // 用户在开放平台的唯一标识符
} }
// Code2SessionResponse 微信code2Session接口响应 // Code2SessionResponse 微信code2Session接口响应
@ -41,7 +47,7 @@ type Code2SessionResponse struct {
// MiniprogramLogin 小程序登录 // MiniprogramLogin 小程序登录
// @Summary 小程序登录 // @Summary 小程序登录
// @Description 通过AppID和code获取用户的openid和session_key // @Description 通过AppID和code获取用户的openid,系统自动生成用户名和头像
// @Tags 微信 // @Tags 微信
// @Accept json // @Accept json
// @Produce json // @Produce json
@ -90,46 +96,150 @@ func (h *handler) MiniprogramLogin() core.HandlerFunc {
return 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.Success = true
res.Message = "登录成功" res.Message = "登录成功"
res.Token = token
res.UserID = user.UserID
res.UserName = user.UserName
res.Avatar = user.UserAvatar
res.OpenID = openID res.OpenID = openID
res.SessionKey = sessionKey
res.UnionID = unionID 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) 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接口 // callCode2Session 调用微信code2Session接口
func (h *handler) callCode2Session(ctx core.Context, appID, appSecret, jsCode string) (string, string, string, error) { func (h *handler) callCode2Session(ctx core.Context, appID, appSecret, jsCode string) (string, string, string, error) {
// 构建请求URL // 构建请求URL

View File

@ -233,6 +233,7 @@ func New(logger logger.CustomLogger, options ...Option) (Mux, error) {
mux.engine.Use(cors.New()) mux.engine.Use(cors.New())
mux.engine.StaticFS("resources", gin.Dir(configs.GetResourcesFilePath(), true)) mux.engine.StaticFS("resources", gin.Dir(configs.GetResourcesFilePath(), true))
mux.engine.StaticFS("static", gin.Dir("static", true))
// withoutTracePaths 这些请求,默认不记录日志 // withoutTracePaths 这些请求,默认不记录日志
withoutTracePaths := map[string]bool{ withoutTracePaths := map[string]bool{

View File

@ -0,0 +1,5 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" fill="#e0e0e0"/>
<circle cx="50" cy="35" r="15" fill="#9e9e9e"/>
<path d="M20 80 Q20 65 35 65 L65 65 Q80 65 80 80 L80 100 L20 100 Z" fill="#9e9e9e"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B