2024-09-14 10:48:09 +08:00
|
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"log"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"qnc-server/config"
|
|
|
|
|
common "qnc-server/model/common"
|
|
|
|
|
model "qnc-server/model/model"
|
|
|
|
|
"qnc-server/model/request"
|
|
|
|
|
"qnc-server/model/response"
|
|
|
|
|
"qnc-server/service"
|
|
|
|
|
"qnc-server/utils"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type User struct {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var userService service.UserService
|
|
|
|
|
var verifyService service.VerifyCode
|
|
|
|
|
|
|
|
|
|
// 注册路由
|
|
|
|
|
func InitUser(group *gin.RouterGroup) {
|
|
|
|
|
var u User
|
|
|
|
|
userPublicGroup := group.Group("user")
|
|
|
|
|
userPrivateGroup := group.Group("user")
|
|
|
|
|
{
|
|
|
|
|
userPublicGroup.GET("test", u.Test)
|
|
|
|
|
userPublicGroup.POST("log", u.Log)
|
|
|
|
|
userPublicGroup.POST("login", u.Login)
|
2024-12-25 11:59:33 +08:00
|
|
|
|
userPublicGroup.POST("tydata_login", u.TyDataLogin)
|
2024-09-14 10:48:09 +08:00
|
|
|
|
userPublicGroup.POST("h5_login", u.H5Login)
|
|
|
|
|
userPublicGroup.POST("phone_login", u.PhoneLogin)
|
|
|
|
|
userPublicGroup.POST("verify", u.GetVerify)
|
|
|
|
|
userPublicGroup.GET("get_config", u.GetSDKConfig)
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
userPrivateGroup.Use(JWTAuth())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
func (u *User) Test(c *gin.Context) {
|
|
|
|
|
authIdentifier := model.AuthIdentifier{
|
|
|
|
|
OpenID: "oR_hJ6kv2bmH7YWPzuZdwQvN4B5g",
|
|
|
|
|
UnionID: "ogzHA6W4T7vHGl-99nlHkSJDczBc",
|
|
|
|
|
}
|
|
|
|
|
authentications, err := userService.MatchingAuthentications(authIdentifier)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.OkWithData(authentications, c)
|
|
|
|
|
}
|
|
|
|
|
func (u *User) Log(c *gin.Context) {
|
|
|
|
|
postForm := c.Request.PostForm
|
|
|
|
|
log.Printf("前端请求打印:%s", postForm)
|
|
|
|
|
response.Ok(c)
|
|
|
|
|
}
|
|
|
|
|
func (u *User) Login(c *gin.Context) {
|
|
|
|
|
var reqBody request.UserLoginReq
|
|
|
|
|
|
|
|
|
|
// 尝试将请求的 JSON body 绑定到 reqBody 结构体
|
|
|
|
|
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
|
|
|
|
response.FailWithMessage(err.Error(), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查请求体中是否包含 code 字段
|
|
|
|
|
if reqBody.Code == "" {
|
|
|
|
|
response.FailWithMessage("code is required", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Code获取用户信息
|
|
|
|
|
wxResponse, err := userService.RequestWx(reqBody.Code)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.FailWithMessage(fmt.Errorf("request weixin err:%w", err).Error(), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
authIdentifier := model.AuthIdentifier{
|
|
|
|
|
OpenID: wxResponse.OpenId,
|
|
|
|
|
UnionID: wxResponse.Unionid,
|
|
|
|
|
}
|
|
|
|
|
// 是否有匹配的校验信息
|
|
|
|
|
authentications, err := userService.MatchingAuthentications(authIdentifier)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("MatchingAuthentications Error:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var authentication model.Authentication
|
|
|
|
|
if len(authentications) == 0 {
|
|
|
|
|
authentication, err = userService.Register(model.User{}, model.AUTHTYPE_WECHAT_MP, authIdentifier)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("注册用户错误:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
//成功创建用户
|
|
|
|
|
} else {
|
|
|
|
|
// 校验信息是否有该平台
|
|
|
|
|
hasWechatMP := false
|
|
|
|
|
for _, auth := range authentications {
|
|
|
|
|
if auth.AuthType == model.AUTHTYPE_WECHAT_MP {
|
|
|
|
|
hasWechatMP = true
|
|
|
|
|
authentication = auth
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 没有则创建该平台校验并关联用户
|
|
|
|
|
if !hasWechatMP {
|
|
|
|
|
userid := authentication.UserID
|
|
|
|
|
authentication, err = userService.CreateAuthentications(userid, authIdentifier, model.AUTHTYPE_WECHAT_MP)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("创建平台校验错误:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
//成功校验该平台成功
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
user, err := userService.GetUser(authentication.UserID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("authentication获取关联用户失败:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
u.TokenNext(c, user, authIdentifier, model.AUTHTYPE_WECHAT_MP)
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-25 11:59:33 +08:00
|
|
|
|
// 天远数据登录
|
|
|
|
|
func (u *User) TyDataLogin(c *gin.Context) {
|
|
|
|
|
var reqBody request.UserLoginReq
|
|
|
|
|
|
|
|
|
|
// 尝试将请求的 JSON body 绑定到 reqBody 结构体
|
|
|
|
|
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
|
|
|
|
response.FailWithMessage(err.Error(), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查请求体中是否包含 code 字段
|
|
|
|
|
if reqBody.Code == "" {
|
|
|
|
|
response.FailWithMessage("code is required", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Code获取用户信息
|
|
|
|
|
wxResponse, err := userService.RequestWxTyData(reqBody.Code)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.FailWithMessage(fmt.Errorf("request weixin err:%w", err).Error(), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
authIdentifier := model.AuthIdentifier{
|
|
|
|
|
OpenID: wxResponse.OpenId,
|
|
|
|
|
UnionID: wxResponse.Unionid,
|
|
|
|
|
}
|
|
|
|
|
// 是否有匹配的校验信息
|
|
|
|
|
authentications, err := userService.MatchingAuthentications(authIdentifier)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("MatchingAuthentications Error:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var authentication model.Authentication
|
|
|
|
|
if len(authentications) == 0 {
|
|
|
|
|
authentication, err = userService.Register(model.User{}, model.AUTHTYPE_TYDATA, authIdentifier)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("注册用户错误:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
//成功创建用户
|
|
|
|
|
} else {
|
|
|
|
|
// 校验信息是否有该平台
|
|
|
|
|
hasWechatMP := false
|
|
|
|
|
for _, auth := range authentications {
|
|
|
|
|
if auth.AuthType == model.AUTHTYPE_TYDATA {
|
|
|
|
|
hasWechatMP = true
|
|
|
|
|
authentication = auth
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 没有则创建该平台校验并关联用户
|
|
|
|
|
if !hasWechatMP {
|
|
|
|
|
userid := authentication.UserID
|
|
|
|
|
authentication, err = userService.CreateAuthentications(userid, authIdentifier, model.AUTHTYPE_TYDATA)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("创建平台校验错误:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
//成功校验该平台成功
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
user, err := userService.GetUser(authentication.UserID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("authentication获取关联用户失败:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
u.TokenNext(c, user, authIdentifier, model.AUTHTYPE_TYDATA)
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 10:48:09 +08:00
|
|
|
|
// 公众号h5登录
|
|
|
|
|
func (u *User) H5Login(c *gin.Context) {
|
|
|
|
|
var reqBody request.H5UserLoginReq
|
|
|
|
|
|
|
|
|
|
// 尝试将请求的 JSON body 绑定到 reqBody 结构体
|
|
|
|
|
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
|
|
|
|
response.FailWithMessage(err.Error(), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if reqBody.UserinfoAuth == nil {
|
|
|
|
|
response.FailWithDetailed(reqBody, "根本没有Auth", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// 检查请求体中是否包含 code 字段
|
|
|
|
|
if reqBody.Code == "" {
|
|
|
|
|
response.FailWithMessage("code is required", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取到h5的openid token
|
|
|
|
|
wxH5Response, err := userService.RequestWxH5(reqBody.Code)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("request weixin err:%v", err)
|
|
|
|
|
response.FailWithMessage(fmt.Sprintf("登录失败,请稍后再试:%v", err), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
authIdentifier := model.AuthIdentifier{
|
|
|
|
|
OpenID: wxH5Response.Openid,
|
|
|
|
|
}
|
|
|
|
|
// 是否有匹配的校验信息
|
|
|
|
|
authentications, err := userService.MatchingAuthentications(authIdentifier)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("GetUserByH5 err:%v", err)
|
|
|
|
|
response.FailWithMessage(fmt.Sprintf("系统错误登录失败,请稍后再试:%v", err), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var authentication model.Authentication
|
|
|
|
|
if len(authentications) == 0 {
|
|
|
|
|
// 是否手动授权登录
|
|
|
|
|
if *reqBody.UserinfoAuth {
|
|
|
|
|
userinfo, err := userService.GetSnsUserInfo(wxH5Response.AccessToken, wxH5Response.Openid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("request weixin getuserinfo err:%v", err)
|
|
|
|
|
response.FailWithMessage(fmt.Sprintf("登录失败,请稍后再试:%v", err), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
log.Printf("UserInfo:%v", userinfo)
|
|
|
|
|
authIdentifier.UnionID = userinfo.UnionID
|
|
|
|
|
log.Printf("authIdentifier:%v", authIdentifier)
|
|
|
|
|
//查找匹配UnionID
|
|
|
|
|
authentications, err = userService.MatchingAuthentications(authIdentifier)
|
|
|
|
|
log.Printf("authentications:%v", authentications)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("get user err:%v", err)
|
|
|
|
|
response.FailWithMessage(fmt.Sprintf("登录失败,请稍后再试:%v", err), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if len(authentications) == 0 {
|
|
|
|
|
//没有则创建用户
|
|
|
|
|
authentication, err = userService.Register(model.User{}, model.AUTHTYPE_WECHAT_H5, authIdentifier)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("注册用户错误:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 有则创建平台校验
|
|
|
|
|
authentication, err = userService.CreateAuthentications(authentications[0].UserID, authIdentifier, model.AUTHTYPE_WECHAT_H5)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("创建平台校验错误:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 静默登录并且库中无openid,让前端重新进行手动授权
|
|
|
|
|
response.ManualAuth(c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for _, auth := range authentications {
|
|
|
|
|
if auth.AuthType == model.AUTHTYPE_WECHAT_H5 {
|
|
|
|
|
authentication = auth
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user, err := userService.GetUser(authentication.UserID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("authentication获取关联用户失败:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
//有则直接登录
|
|
|
|
|
u.TokenNext(c, user, authIdentifier, model.AUTHTYPE_WECHAT_H5)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 手机号登录
|
|
|
|
|
func (u *User) PhoneLogin(c *gin.Context) {
|
|
|
|
|
var reqBody request.PhoneLoginReq
|
|
|
|
|
// 尝试将请求的 JSON body 绑定到 reqBody 结构体
|
|
|
|
|
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
|
|
|
|
response.FailWithMessage(err.Error(), c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 校验验证码
|
|
|
|
|
isRight, err := utils.VerifyCode(reqBody.PhoneNumber, reqBody.VerifyCode)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.FailWithMessage(err.Error(), c)
|
|
|
|
|
return
|
|
|
|
|
} else if !isRight {
|
|
|
|
|
response.FailWithMessage("验证码错误", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
authIdentifier := model.AuthIdentifier{
|
|
|
|
|
Phone: reqBody.PhoneNumber,
|
|
|
|
|
}
|
|
|
|
|
authentications, err := userService.MatchingAuthentications(authIdentifier)
|
|
|
|
|
|
|
|
|
|
var authentication model.Authentication
|
|
|
|
|
if len(authentications) == 0 {
|
|
|
|
|
authentication, err = userService.Register(model.User{Phone: reqBody.PhoneNumber}, model.AUTHTYPE_PHONE, authIdentifier)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("注册用户错误:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
//成功创建用户
|
|
|
|
|
} else {
|
|
|
|
|
authentication = authentications[0]
|
|
|
|
|
}
|
|
|
|
|
user, err := userService.GetUser(authentication.UserID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("authentication获取关联用户失败:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
u.TokenNext(c, user, authIdentifier, model.AUTHTYPE_PHONE)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TokenNext 登录以后签发jwt
|
|
|
|
|
func (u *User) TokenNext(c *gin.Context, user *model.User, authIdentifiers model.AuthIdentifier, platform model.AuthType) {
|
|
|
|
|
j := &utils.JWT{SigningKey: []byte(config.ConfigData.JWT.SigningKey)} // 唯一签名
|
|
|
|
|
claims := j.CreateClaims(common.BaseClaims{
|
|
|
|
|
Userid: user.Userid,
|
|
|
|
|
Disable: user.Disable != nil && *user.Disable,
|
|
|
|
|
AuthIdentifiers: authIdentifiers,
|
|
|
|
|
Platform: platform,
|
|
|
|
|
})
|
|
|
|
|
token, err := j.CreateToken(claims)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.FailWithMessage("获取token失败", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix()))
|
|
|
|
|
response.OkWithDetailed(response.LoginResponse{
|
|
|
|
|
User: *user,
|
|
|
|
|
//AuthIdentifiers: authIdentifiers,
|
|
|
|
|
Token: token,
|
|
|
|
|
ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000,
|
|
|
|
|
}, "登录成功", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取验证码
|
|
|
|
|
func (u *User) GetVerify(c *gin.Context) {
|
|
|
|
|
var req request.VerifyCodeReq
|
|
|
|
|
|
|
|
|
|
// 绑定JSON数据到结构体
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
response.FailWithMessage("PhoneNumber is required", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证手机号码格式
|
|
|
|
|
if !utils.IsValidPhoneNumber(req.PhoneNumber) {
|
|
|
|
|
response.FailWithMessage("Invalid phone number format", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
existsCode, err := utils.CanRequestCode(req.PhoneNumber)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if existsCode {
|
|
|
|
|
response.FailWithMessage("请求过于频繁,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var code = utils.GenerateVerificationCode()
|
|
|
|
|
|
|
|
|
|
defaultTemplateCode := config.ConfigData.VerifyCode.TemplateCode
|
|
|
|
|
if req.Template == "marriage" {
|
|
|
|
|
defaultTemplateCode = config.ConfigData.VerifyCode.MarriageTemplateCode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = verifyService.Send(code, req.PhoneNumber, defaultTemplateCode)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("【获取验证码】请求验证码发送失败:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
err = utils.StoreVerificationCode(req.PhoneNumber, code)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("【获取验证码】验证码缓存失败:%v", err)
|
|
|
|
|
response.FailWithMessage("系统错误,请稍后再试", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.OkWithMessage("获取成功", c)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// h5 sdk config
|
|
|
|
|
func (u *User) GetSDKConfig(c *gin.Context) {
|
|
|
|
|
pageURL := c.Query("url")
|
|
|
|
|
if pageURL == "" {
|
|
|
|
|
log.Printf("GetSDKConfig url 没传")
|
|
|
|
|
response.FailWithMessage("系统错误", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ticket, err := userService.GetJsapiTicket()
|
|
|
|
|
if err != nil || ticket == "" {
|
|
|
|
|
log.Printf("get jsapi ticket err:%v", err)
|
|
|
|
|
response.FailWithMessage("获取配置失败", c)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nonceStr := strconv.Itoa(rand.Int())
|
|
|
|
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
|
|
|
|
signature := userService.CreateSignature(ticket, nonceStr, timestamp, pageURL)
|
|
|
|
|
|
|
|
|
|
response.OkWithData(gin.H{
|
|
|
|
|
"ticket": ticket,
|
|
|
|
|
"appId": config.ConfigData.System.WxH5AppId,
|
|
|
|
|
"timestamp": timestamp,
|
|
|
|
|
"nonceStr": nonceStr,
|
|
|
|
|
"signature": signature,
|
|
|
|
|
}, c)
|
|
|
|
|
}
|