🚧 improve image encryption and decryption

This commit is contained in:
2025-03-22 13:05:50 +08:00
parent 3a03224f8c
commit 781a71a27c
39 changed files with 1274 additions and 977 deletions

View File

@@ -45,6 +45,12 @@ type (
Avatar string `json:"avatar"`
Status int64 `json:"status"`
}
AdminLoginRequest {
Account string `json:"account"`
Password string `json:"password"`
Dots string `json:"dots"`
Key string `json:"key"`
}
)
// OAuth请求参数
@@ -95,6 +101,11 @@ type (
ThumbX int64 `json:"thumb_x"`
ThumbY int64 `json:"thumb_y"`
}
TextCaptchaResponse {
Key string `json:"key"`
Image string `json:"image"`
Thumb string `json:"thumb"`
}
)
// 用户服务
@@ -128,6 +139,10 @@ service auth {
// 获取微信公众号二维码
@handler getWechatOffiaccountQrcode
post /wechat/offiaccount/qrcode (OAuthWechatRequest) returns (string)
// 管理员登录
@handler adminLogin
post /admin/login (AdminLoginRequest) returns (LoginResponse)
}
@server (
@@ -250,6 +265,10 @@ service auth {
@handler generateSlideBasicCaptcha
get /slide/generate returns (SlideCaptchaResponse)
// 文字点选验证码
@handler generateTextCaptcha
get /text/generate returns (TextCaptchaResponse)
}
type (
@@ -731,12 +750,23 @@ type (
Provider string `json:"provider"`
Bucket string `json:"bucket"`
Password string `json:"password"`
Dots string `json:"dots"`
Key string `json:"key"`
}
SinglePrivateImageRequest {
ID int64 `json:"id"`
Password string `json:"password"`
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
CoordinateMeta {
ID int64 `json:"id"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
ImageCount int64 `json:"image_count"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
}
CoordinateListResponse {
Records []CoordinateMeta `json:"records"`
@@ -884,6 +914,10 @@ service auth {
@handler getPrivateImageList
post /image/private/list (PrivateImageListRequest) returns (AllImageListResponse)
// 获取解密单个隐私加密图片
@handler getPrivateImageUrl
post /image/private/url/single (SinglePrivateImageRequest) returns (string)
// 获取图像经纬度列表
@handler getCoordinateList
post /coordinate/list returns (CoordinateListResponse)
@@ -1037,3 +1071,37 @@ service auth {
post /logout returns (string)
}
type (
UserMeta {
ID int64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Phone string `json:"phone"`
Status int64 `json:"status"`
CreatedAt string `json:"created_at"`
}
UserInfoListResponse {
Records []UserMeta `json:"records"`
}
)
// 系统服务
@server (
group: system // 微服务分组
prefix: /api/auth/system // 微服务前缀
timeout: 10s // 超时时间
maxBytes: 10485760 // 最大请求大小
signature: true // 是否开启签名验证
middleware: SecurityHeadersMiddleware,CasbinVerifyMiddleware,NonceMiddleware,AuthMiddleware // 注册中间件
MaxConns: true // 是否开启最大连接数限制
Recover: true // 是否开启自动恢复
jwt: Auth // 是否开启jwt验证
)
service auth {
// 获取用户列表
@handler getUserList
post /user/list returns (UserInfoListResponse)
}

View File

@@ -90,6 +90,8 @@ Signature:
Encrypt:
# 密钥32
Key: p3380puliiep184buh8d5dvujeerqtem
PublicKey: api/etc/common_rsa_public_key.pem
PrivateKey: api/etc/common_rsa_private_key.pem
# Redis 配置
Redis:
# Redis 地址

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAseOyY9ETmhkaSfUj9LqvU9/+Jo81a+pKGgKGBPB0If+YkCH0
Frw1mCtZoCsidZeEMXtSaS1U/lbmKs2L6vExcZr8uSAGb2NXuQn+y8wPnEjAvsU7
KaP/cUK11R/6JRHQes5izliNQfdUdnlSuN0X9PFoozuaELb0+SUTTLKkd1mnDk7x
3bMN6CNNsTg/zT5hYeuGkunyr5QA4YpofnBC8YID6YlD/mw7x8S4JRPFCmq58Hgq
+C2eW716PibyMbCftWMVASvy/OgxnRwvJVho/Br14YiDl3qQWiUPxUYcE1mUrW5t
qjARKc0OX5Owid+/ydwtxg8hi/yTskUfjUnXtQIDAQABAoIBACD3MkrfJwPKnR2R
iT1ED1O60c1xgpPiEiNpzk5CBTN7u1kSgbpo3IG7nttYwwUJtBy7XtVQ6kxL7FGI
T+KVGfWUpDrmXWrs/Qe0e3xm74mlzdpMkJ8x3heuJiY9y8xs1ba8YoEc1eigng1q
hFLv3g2tYxfE5tMsJI+7OC1heasIKd88ZO170/mLpDzaKFI0+tUlRCyOIfP8ltCE
Bm+1jDeVeWI3HTWESbEX8+eCA/av/h0/JBmtvqZpK2ZYLQMBv9t0ekVqSmm6T9Mj
cy/S9Lb7qXoW/2m4QFkD9ph9RY5HJWNZcEKwJNs3ecJJiMD52aGA9obqH9GH5SBj
yDq42eECgYEAxTlYngXh3cQ81/KcZxJE9ZoFnmrKeqMX7g4OUyTzLDhUk7K+oGZI
wn+K31GpDgC5fpjLsvpKVsnjOjluTLcnRsJ0OUyITHNQo4Z/FzgxwNLnsM99+h3E
/75/3xqwnoCWuRSvZHKD6Zk/uBu1UV8rsCJRwUYvzIAVwZxd0Aabjm0CgYEA5udG
ILV20HTZq3auHj1x16idhB8Nemg1GyD+hNxzVLZ8Zkrj3HjjukJv1FOOOPkKhmtl
3nrYX/8gwxh7HEC+coi7WQqbbvlLIiKaAlYJB7DKip02n3dlzUmp9kpB5EcjJuvX
hDrN/s/i6q9M3XWP2zl0oLwkv/GJzs/3cZ7mAWkCgYALcpSuN3Ewyh8t+asSYIEY
MGR7GX+/NpBBBRfXw6FJw8tE928RKF64y2ZoJ/lEEs6xhnTsYpLGDtndm0/HrCnf
dZIBcWvH5DmeBESEOILKynMgVCrfxbKVlZ0eehIeYSBehdDYZ704ZejI6vLPUlLa
2mMccNJ9cEHTBxx64qdM0QKBgBELpauoebrtxVvZCQWGd6757ZbhS/drVfBIwUFB
nOn2Brzubl/KNNV9LhA4ktk12UcPCpgf7XU4ukxstDnjtaty2JG8LLlGgftlHoVp
oIUG0gzlijC/ea5r77YUyUR20+t9oY1LYgWbhx7YDg6TLSl71lY/TV82D3xK8fNb
TZNxAoGAZQlW9+UMOtptfzOh4HLeEs9y2/KvlfUYClgcKyz8FuGeuOMyf8bgFK8L
OzGNkEFOgU1CQ9j6de3Id996qK1sGiZwyBVCEXqnnLLUk3yUGbO32wVXniJSrrlT
0bekyTJEjnol2E4Rv3WrX7ddGunU2R7p35HkXoWBXHGfwd7F3eI=
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN RSA PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAseOyY9ETmhkaSfUj9Lqv
U9/+Jo81a+pKGgKGBPB0If+YkCH0Frw1mCtZoCsidZeEMXtSaS1U/lbmKs2L6vEx
cZr8uSAGb2NXuQn+y8wPnEjAvsU7KaP/cUK11R/6JRHQes5izliNQfdUdnlSuN0X
9PFoozuaELb0+SUTTLKkd1mnDk7x3bMN6CNNsTg/zT5hYeuGkunyr5QA4YpofnBC
8YID6YlD/mw7x8S4JRPFCmq58Hgq+C2eW716PibyMbCftWMVASvy/OgxnRwvJVho
/Br14YiDl3qQWiUPxUYcE1mUrW5tqjARKc0OX5Owid+/ydwtxg8hi/yTskUfjUnX
tQIDAQAB
-----END RSA PUBLIC KEY-----

View File

@@ -15,7 +15,9 @@ type Config struct {
AccessSecret string
}
Encrypt struct {
Key string
Key string
PublicKey string
PrivateKey string
}
Mysql struct {
DataSource string

View File

@@ -0,0 +1,21 @@
package captcha
import (
"net/http"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/captcha"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/common/xhttp"
)
func GenerateTextCaptchaHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := captcha.NewGenerateTextCaptchaLogic(r.Context(), svcCtx)
resp, err := l.GenerateTextCaptcha()
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -16,6 +16,7 @@ import (
share "schisandra-album-cloud-microservices/app/auth/api/internal/handler/share"
sms "schisandra-album-cloud-microservices/app/auth/api/internal/handler/sms"
storage "schisandra-album-cloud-microservices/app/auth/api/internal/handler/storage"
system "schisandra-album-cloud-microservices/app/auth/api/internal/handler/system"
token "schisandra-album-cloud-microservices/app/auth/api/internal/handler/token"
user "schisandra-album-cloud-microservices/app/auth/api/internal/handler/user"
websocket "schisandra-album-cloud-microservices/app/auth/api/internal/handler/websocket"
@@ -61,6 +62,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/slide/generate",
Handler: captcha.GenerateSlideBasicCaptchaHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/text/generate",
Handler: captcha.GenerateTextCaptchaHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/captcha"),
@@ -395,6 +401,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/image/private/list",
Handler: storage.GetPrivateImageListHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/image/private/url/single",
Handler: storage.GetPrivateImageUrlHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/image/recent/list",
@@ -453,6 +464,24 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithMaxBytes(104857600),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.CasbinVerifyMiddleware, serverCtx.NonceMiddleware, serverCtx.AuthMiddleware},
[]rest.Route{
{
Method: http.MethodPost,
Path: "/user/list",
Handler: system.GetUserListHandler(serverCtx),
},
}...,
),
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
rest.WithSignature(serverCtx.Config.Signature),
rest.WithPrefix("/api/auth/system"),
rest.WithTimeout(10000*time.Millisecond),
rest.WithMaxBytes(10485760),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.CasbinVerifyMiddleware, serverCtx.NonceMiddleware},
@@ -474,6 +503,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware},
[]rest.Route{
{
Method: http.MethodPost,
Path: "/admin/login",
Handler: user.AdminLoginHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/login",

View File

@@ -0,0 +1,29 @@
package storage
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/storage"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/common/xhttp"
)
func GetPrivateImageUrlHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SinglePrivateImageRequest
if err := httpx.Parse(r, &req); err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
return
}
l := storage.NewGetPrivateImageUrlLogic(r.Context(), svcCtx)
resp, err := l.GetPrivateImageUrl(&req)
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,21 @@
package system
import (
"net/http"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/system"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/common/xhttp"
)
func GetUserListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := system.NewGetUserListLogic(r.Context(), svcCtx)
resp, err := l.GetUserList()
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,29 @@
package user
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/user"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/common/xhttp"
)
func AdminLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminLoginRequest
if err := httpx.Parse(r, &req); err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
return
}
l := user.NewAdminLoginLogic(r.Context(), svcCtx)
resp, err := l.AdminLogin(r, &req)
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,39 @@
package captcha
import (
"context"
"net/http"
"schisandra-album-cloud-microservices/common/captcha/generate"
"schisandra-album-cloud-microservices/common/errors"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GenerateTextCaptchaLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGenerateTextCaptchaLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateTextCaptchaLogic {
return &GenerateTextCaptchaLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GenerateTextCaptchaLogic) GenerateTextCaptcha() (resp *types.TextCaptchaResponse, err error) {
captcha, err := generate.GenerateBasicTextCaptcha(l.svcCtx.TextCaptcha, l.svcCtx.RedisClient, l.ctx)
if err != nil {
return nil, errors.New(http.StatusInternalServerError, err.Error())
}
return &types.TextCaptchaResponse{
Key: captcha["key"].(string),
Image: captcha["image"].(string),
Thumb: captcha["thumb"].(string),
}, nil
}

View File

@@ -49,8 +49,8 @@ func (l *DeleteShareRecordLogic) DeleteShareRecord(req *types.DeleteShareRecordR
return "", errors.New("delete share record failed")
}
shareVisit := tx.ScaStorageShareVisit
shareVisitDeleted, err := shareVisit.Where(shareVisit.ShareID.Eq(req.ID), shareVisit.UserID.Eq(uid)).Delete()
if err != nil || shareVisitDeleted.RowsAffected == 0 {
_, err = shareVisit.Where(shareVisit.ShareID.Eq(req.ID), shareVisit.UserID.Eq(uid)).Delete()
if err != nil {
tx.Rollback()
return "", errors.New("delete share visit record failed")
}

View File

@@ -54,23 +54,25 @@ func (l *QueryShareImageLogic) QueryShareImage(req *types.QueryShareImageRequest
shareData, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.New("share code not found")
return nil, errors.New("没有找到分享记录")
}
return nil, err
}
var storageShare model.ScaStorageShare
if err := json.Unmarshal([]byte(shareData), &storageShare); err != nil {
return nil, errors.New("unmarshal share data failed")
return nil, errors.New("解析分享记录失败")
}
// 验证密码
if storageShare.AccessPassword != "" && storageShare.AccessPassword != req.AccessPassword {
return nil, errors.New("incorrect password")
return nil, errors.New("密码错误")
}
// 检查分享是否过期
if storageShare.ExpireTime.Before(time.Now()) {
return nil, errors.New("share link has expired")
if storageShare.ValidityPeriod > 0 {
if storageShare.ExpireTime.Before(time.Now()) {
return nil, errors.New("分享已过期")
}
}
// 检查访问限制

View File

@@ -87,7 +87,10 @@ func (l *UploadShareImageLogic) UploadShareImage(req *types.ShareImageRequest) (
if err != nil {
return "", errors.New("invalid expire date")
}
expiryTime := l.GenerateExpiryTime(time.Now(), duration)
var expiryTime time.Time
if duration > 0 {
expiryTime = l.GenerateExpiryTime(time.Now(), duration)
}
storageShare := model.ScaStorageShare{
UserID: uid,
AlbumID: album.ID,

View File

@@ -35,13 +35,18 @@ func (l *GetCoordinateListLogic) GetCoordinateList() (resp *types.CoordinateList
storageLocation.ID,
storageLocation.Longitude,
storageLocation.Latitude,
storageLocation.Country,
storageLocation.Province,
storageLocation.City,
storageInfo.ID.Count().As("image_count"),
).Join(
storageInfo,
storageLocation.ID.EqCol(storageInfo.LocationID),
).Where(storageLocation.UserID.Eq(uid),
storageInfo.UserID.Eq(uid),
).Scan(&records)
).
Group(storageLocation.ID).
Scan(&records)
if err != nil {
return nil, err
}

View File

@@ -1,24 +1,20 @@
package storage
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"gorm.io/gen"
"io"
"math/rand"
"net/http"
"gorm.io/gorm"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/captcha/verify"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"schisandra-album-cloud-microservices/common/utils"
"sort"
"sync"
"time"
@@ -31,9 +27,8 @@ import (
type GetPrivateImageListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
RestyClient *resty.Client
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPrivateImageListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPrivateImageListLogic {
@@ -41,16 +36,6 @@ func NewGetPrivateImageListLogic(ctx context.Context, svcCtx *svc.ServiceContext
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
RestyClient: resty.New().
SetTimeout(30 * time.Second). // 总超时时间
SetRetryCount(3). // 重试次数
SetRetryWaitTime(5 * time.Second). // 重试等待时间
SetRetryMaxWaitTime(30 * time.Second). // 最大重试等待
AddRetryCondition(func(r *resty.Response, err error) bool {
return r.StatusCode() == http.StatusTooManyRequests ||
err != nil ||
r.StatusCode() >= 500
}),
}
}
@@ -59,8 +44,29 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
if !ok {
return nil, errors.New("user_id not found")
}
captcha := verify.VerifyBasicTextCaptcha(req.Dots, req.Key, l.svcCtx.RedisClient, l.ctx)
if !captcha {
return nil, errors.New("验证错误")
}
if req.Password == "" {
return nil, errors.New("密码不能为空")
}
authUser := l.svcCtx.DB.ScaAuthUser
userInfo, err := authUser.
Select(authUser.UID, authUser.Password).
Where(authUser.UID.Eq(uid)).First()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if userInfo == nil {
return nil, errors.New("密码错误")
}
if !utils.Verify(userInfo.Password, req.Password) {
return nil, errors.New("密码错误")
}
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
conditions := []gen.Condition{
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
@@ -74,10 +80,16 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageInfo.Path).
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize,
).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
Where(conditions...).
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
if err != nil {
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if len(storageInfoList) == 0 {
@@ -110,48 +122,15 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
g.Go(func() error {
defer sem.Release(1)
// 生成单条缓存键(包含文件唯一标识)
imageCacheKey := fmt.Sprintf("%s%s:%s:%s:%s:%v",
constant.ImageCachePrefix,
uid,
"list",
req.Provider,
req.Bucket,
dbFileInfo.ID)
// 尝试获取单条缓存
if cached, err := l.svcCtx.RedisClient.Get(l.ctx, imageCacheKey).Result(); err == nil {
var meta types.ImageMeta
if err := json.Unmarshal([]byte(cached), &meta); err == nil {
parse, err := time.Parse("2006-01-02 15:04:05", meta.CreatedAt)
if err == nil {
logx.Error("Parse Time Error:", err)
return nil
}
date := parse.Format("2006年1月2日 星期") + WeekdayMap[parse.Weekday()]
value, _ := groupedImages.LoadOrStore(date, []types.ImageMeta{})
images := value.([]types.ImageMeta)
images = append(images, meta)
groupedImages.Store(date, images)
return nil
}
}
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Minute*30)
if err != nil {
logx.Error(err)
return err
}
imageBytes, err := l.DownloadAndDecrypt(l.ctx, url, uid)
if err != nil {
logx.Error(err)
return err
}
imageData, err := l.svcCtx.XCipher.Decrypt(imageBytes, []byte(uid))
thumbnailUrl, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.ThumbPath, time.Minute*30)
if err != nil {
logx.Error(err)
return err
}
// 使用 Load 或 Store 确保原子操作
value, _ := groupedImages.LoadOrStore(date, []types.ImageMeta{})
images := value.([]types.ImageMeta)
@@ -159,7 +138,7 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
URL: base64.StdEncoding.EncodeToString(imageData),
Thumbnail: thumbnailUrl,
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
@@ -167,14 +146,6 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
// 缓存单条数据24小时基础缓存 + 随机防雪崩)
if data, err := json.Marshal(images); err == nil {
expire := 24*time.Hour + time.Duration(rand.Intn(3600))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, imageCacheKey, data, expire).Err(); err != nil {
logx.Error("Failed to cache image meta:", err)
}
}
return nil
})
}
@@ -263,27 +234,3 @@ func (l *GetPrivateImageListLogic) getOssConfigFromCacheOrDb(cacheKey, uid, prov
return ossConfig, nil
}
func (l *GetPrivateImageListLogic) DownloadAndDecrypt(ctx context.Context, url string, uid string) ([]byte, error) {
resp, err := l.RestyClient.R().
SetContext(ctx).
SetDoNotParseResponse(true). // 保持原始响应流
Get(url)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.RawBody().Close()
if resp.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode(), resp.Status())
}
// 使用缓冲区分块读取
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, resp.RawBody()); err != nil {
return nil, fmt.Errorf("read response body failed: %w", err)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,218 @@
package storage
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"io"
"net/http"
"os"
"path/filepath"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
"schisandra-album-cloud-microservices/common/hybrid_encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"schisandra-album-cloud-microservices/common/utils"
"time"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetPrivateImageUrlLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
RestyClient *resty.Client
}
func NewGetPrivateImageUrlLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPrivateImageUrlLogic {
return &GetPrivateImageUrlLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
RestyClient: resty.New().
SetTimeout(30 * time.Second). // 总超时时间
SetRetryCount(3). // 重试次数
SetRetryWaitTime(5 * time.Second). // 重试等待时间
SetRetryMaxWaitTime(30 * time.Second). // 最大重试等待
AddRetryCondition(func(r *resty.Response, err error) bool {
return r.StatusCode() == http.StatusTooManyRequests ||
err != nil ||
r.StatusCode() >= 500
}),
}
}
// 修改函数签名和实现
func (l *GetPrivateImageUrlLogic) GetPrivateImageUrl(req *types.SinglePrivateImageRequest) (string, error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return "", errors.New("user_id not found")
}
// 构建缓存key
cacheKey := fmt.Sprintf("%s%s:%s:%v", constant.ImageCachePrefix, uid, "encrypted", req.ID)
// 检查缓存
if cachedData, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result(); err == nil {
return cachedData, nil
}
storageInfo := l.svcCtx.DB.ScaStorageInfo
authUser := l.svcCtx.DB.ScaAuthUser
var result struct {
ID int64 `json:"id"`
Path string `json:"path"`
Password string `json:"password"`
FileType string `json:"file_type"`
}
err := storageInfo.
Select(
storageInfo.ID,
storageInfo.Path,
storageInfo.FileType,
authUser.Password).
LeftJoin(authUser, authUser.UID.EqCol(storageInfo.UserID)).
Where(storageInfo.ID.Eq(req.ID), storageInfo.UserID.Eq(uid),
storageInfo.IsEncrypted.Eq(constant.Encrypt), storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket), authUser.UID.Eq(uid)).
Group(storageInfo.ID).Scan(&result)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
if result.ID == 0 {
return "", errors.New("image not found")
}
verify := utils.Verify(result.Password, req.Password)
if !verify {
return "", errors.New("invalid password")
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
if err != nil {
return "", err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return "", errors.New("get storage failed")
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, result.Path, time.Minute*15)
if err != nil {
logx.Error(err)
return "", errors.New("get private image url failed")
}
resp, err := l.RestyClient.R().
SetContext(l.ctx).
SetDoNotParseResponse(true). // 保持原始响应流
Get(url)
if err != nil {
return "", fmt.Errorf("download private image failed: %w", err)
}
defer resp.RawBody().Close()
body, err := io.ReadAll(resp.RawBody())
if err != nil {
return "", err
}
dir, err := os.Getwd()
if err != nil {
return "", err
}
privateKeyPath := filepath.Join(dir, l.svcCtx.Config.Encrypt.PrivateKey)
privateKey, err := os.ReadFile(privateKeyPath)
if err != nil {
return "", err
}
pem, err := hybrid_encrypt.ImportPrivateKeyPEM(privateKey)
if err != nil {
return "", err
}
image, err := hybrid_encrypt.DecryptImage(pem, body)
if err != nil {
return "", err
}
base64Str := base64.StdEncoding.EncodeToString(image)
// 设置缓存过期时间设为12小时
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, base64Str, 12*time.Hour).Err()
if err != nil {
logx.Errorf("cache private image failed: %v", err)
}
return base64Str, nil
}
// 提取解密操作为函数
func (l *GetPrivateImageUrlLogic) decryptConfig(config *model.ScaStorageConfig) (*storageConfig.StorageConfig, error) {
accessKey, err := encrypt.Decrypt(config.AccessKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt access key failed")
}
secretKey, err := encrypt.Decrypt(config.SecretKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt secret key failed")
}
return &storageConfig.StorageConfig{
Provider: config.Provider,
Endpoint: config.Endpoint,
AccessKey: accessKey,
SecretKey: secretKey,
BucketName: config.Bucket,
Region: config.Region,
}, nil
}
// 从缓存或数据库中获取 OSS 配置
func (l *GetPrivateImageUrlLogic) getOssConfigFromCacheOrDb(cacheKey, uid, provider string) (*storageConfig.StorageConfig, error) {
result, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
return nil, errors.New("get oss config failed")
}
var ossConfig *storageConfig.StorageConfig
if result != "" {
var redisOssConfig model.ScaStorageConfig
if err = json.Unmarshal([]byte(result), &redisOssConfig); err != nil {
return nil, errors.New("unmarshal oss config failed")
}
return l.decryptConfig(&redisOssConfig)
}
// 缓存未命中,从数据库中加载
scaOssConfig := l.svcCtx.DB.ScaStorageConfig
dbOssConfig, err := scaOssConfig.Where(scaOssConfig.UserID.Eq(uid), scaOssConfig.Provider.Eq(provider)).First()
if err != nil {
return nil, err
}
// 缓存数据库配置
ossConfig, err = l.decryptConfig(dbOssConfig)
if err != nil {
return nil, err
}
marshalData, err := json.Marshal(dbOssConfig)
if err != nil {
return nil, errors.New("marshal oss config failed")
}
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, marshalData, 0).Err()
if err != nil {
return nil, errors.New("set oss config failed")
}
return ossConfig, nil
}

View File

@@ -14,6 +14,7 @@ import (
"io"
"mime/multipart"
"net/http"
"os"
"path"
"path/filepath"
"schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
@@ -22,6 +23,7 @@ import (
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
"schisandra-album-cloud-microservices/common/hybrid_encrypt"
"schisandra-album-cloud-microservices/common/storage/config"
"strings"
"sync"
@@ -50,13 +52,16 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
if err != nil {
return "", err
}
// 解析上传配置信息
settingResult, err := l.parseUploadSettingResult(r)
if err != nil {
return "", err
}
// 解析上传的文件
file, header, err := l.getUploadedFile(r)
if err != nil {
return "", err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
@@ -76,12 +81,6 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
return "", err
}
// 解析上传配置信息
settingResult, err := l.parseUploadSettingResult(r)
if err != nil {
return "", err
}
// 使用 `errgroup.Group` 处理并发任务
var (
faceId int64
@@ -110,13 +109,25 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
return nil
})
}
var imageBytes []byte
var uploadReader io.Reader = bytes.NewReader(data)
if settingResult.Encrypt {
encryptedData, err := l.svcCtx.XCipher.Encrypt(data, []byte(uid))
dir, err := os.Getwd()
if err != nil {
return "", err
}
imageBytes = encryptedData
publicKeyPath := filepath.Join(dir, l.svcCtx.Config.Encrypt.PublicKey)
publicKey, err := os.ReadFile(publicKeyPath)
if err != nil {
return "", err
}
pem, err := hybrid_encrypt.ImportPublicKeyPEM(publicKey)
if err != nil {
return "", err
}
image, err := hybrid_encrypt.EncryptImage(pem, data)
uploadReader = bytes.NewReader(image)
}
// 上传文件到 OSS
@@ -126,7 +137,7 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
}
defer sem.Release(1)
fileUrl, thumbUrl, err := l.uploadFileToOSS(uid, header, bytes.NewReader(imageBytes), thumbnail, result)
fileUrl, thumbUrl, err := l.uploadFileToOSS(uid, header, uploadReader, thumbnail, result, settingResult)
if err != nil {
return err
}
@@ -210,7 +221,7 @@ func (l *UploadFileLogic) parseUploadSettingResult(r *http.Request) (types.Uploa
}
// 上传文件到 OSS
func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file io.Reader, thumbnail io.Reader, result types.File) (string, string, error) {
func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file io.Reader, thumbnail io.Reader, result types.File, settingResult types.UploadSetting) (string, string, error) {
cacheKey := constant.UserOssConfigPrefix + uid + ":" + result.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheKey, uid, result.Provider)
if err != nil {
@@ -220,7 +231,6 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
if err != nil {
return "", "", errors.New("get storage failed")
}
objectKey := path.Join(
constant.ImageSpace,
uid,
@@ -228,6 +238,15 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
l.classifyFile(result.FileType, result.IsScreenshot),
fmt.Sprintf("%s_%s%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid(), filepath.Ext(header.Filename)),
)
if settingResult.Encrypt {
objectKey = path.Join(
constant.ImageSpace,
uid,
time.Now().Format("2006/01"), // 按年/月划分目录
"encrypted",
fmt.Sprintf("%s_%s%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid(), ".enc"),
)
}
_, err = service.UploadFileSimple(l.ctx, ossConfig.BucketName, objectKey, file, map[string]string{
"Content-Type": header.Header.Get("Content-Type"),
@@ -241,7 +260,7 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
uid,
time.Now().Format("2006/01"), // 按年/月划分目录
l.classifyFile(result.FileType, result.IsScreenshot),
fmt.Sprintf("%s_%s%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid(), filepath.Ext(header.Filename)),
fmt.Sprintf("%s_%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid()),
)
_, err = service.UploadFileSimple(l.ctx, ossConfig.BucketName, thumbObjectKey, thumbnail, map[string]string{
"Content-Type": header.Header.Get("Content-Type"),
@@ -249,6 +268,7 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
if err != nil {
return "", "", errors.New("upload thumbnail file failed")
}
return objectKey, thumbObjectKey, nil
}

View File

@@ -0,0 +1,30 @@
package system
import (
"context"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUserListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetUserListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserListLogic {
return &GetUserListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUserListLogic) GetUserList() (resp *types.UserInfoListResponse, err error) {
// todo: add your logic here and delete this line
return
}

View File

@@ -0,0 +1,59 @@
package user
import (
"context"
"gorm.io/gorm"
"net/http"
"schisandra-album-cloud-microservices/common/captcha/verify"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/errors"
"schisandra-album-cloud-microservices/common/i18n"
"schisandra-album-cloud-microservices/common/utils"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type AdminLoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminLoginLogic {
return &AdminLoginLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AdminLoginLogic) AdminLogin(r *http.Request, req *types.AdminLoginRequest) (resp *types.LoginResponse, err error) {
captcha := verify.VerifyBasicTextCaptcha(req.Dots, req.Key, l.svcCtx.RedisClient, l.ctx)
if !captcha {
return nil, errors.New(http.StatusInternalServerError, i18n.FormatText(l.ctx, "captcha.verificationFailure"))
}
authUser := l.svcCtx.DB.ScaAuthUser
permissionRule := l.svcCtx.DB.ScaAuthPermissionRule
adminUser, err := authUser.
LeftJoin(permissionRule, authUser.UID.EqCol(permissionRule.V0)).
Where(authUser.Username.Eq(req.Account), authUser.Password.Eq(req.Password), permissionRule.V1.Eq(constant.Admin)).
Group(authUser.UID).First()
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if adminUser == nil {
return nil, errors.New(http.StatusInternalServerError, i18n.FormatText(l.ctx, "login.notPermission"))
}
if !utils.Verify(adminUser.Password, req.Password) {
return nil, errors.New(http.StatusInternalServerError, i18n.FormatText(l.ctx, "login.invalidPassword"))
}
data, err := HandleLoginJWT(adminUser, l.svcCtx, true, r, l.ctx)
if err != nil {
return nil, err
}
return data, nil
}

View File

@@ -60,7 +60,11 @@ func (l *PhoneLoginLogic) PhoneLogin(r *http.Request, req *types.PhoneLoginReque
if userInfo == nil {
uid := idgen.NextId()
uidStr := strconv.FormatInt(uid, 10)
avatar := utils2.GenerateAvatar(uidStr)
avatar, err := l.svcCtx.PN.Generate(uidStr, false).ToBase64()
if err != nil {
tx.Rollback()
return nil, err
}
name := randomname.GenerateName()
male := constant2.Male
user := &model.ScaAuthUser{

View File

@@ -12,7 +12,6 @@ import (
errors2 "schisandra-album-cloud-microservices/common/errors"
"schisandra-album-cloud-microservices/common/i18n"
"schisandra-album-cloud-microservices/common/random_name"
"schisandra-album-cloud-microservices/common/utils"
"strconv"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
@@ -58,7 +57,11 @@ func (l *WechatOffiaccountLoginLogic) WechatOffiaccountLogin(r *http.Request, re
// 创建用户
uid := idgen.NextId()
uidStr := strconv.FormatInt(uid, 10)
avatar := utils.GenerateAvatar(uidStr)
avatar, err := l.svcCtx.PN.Generate(uidStr, false).ToBase64()
if err != nil {
tx.Rollback()
return nil, err
}
name := randomname.GenerateName()
addUser := &model2.ScaAuthUser{

View File

@@ -3,11 +3,12 @@ package svc
import (
"github.com/ArtisanCloud/PowerWeChat/v3/src/officialAccount"
"github.com/casbin/casbin/v2"
"github.com/landaiqing/go-xcipher"
"github.com/landaiqing/go-pixelnebula"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/minio/minio-go/v7"
"github.com/nsqio/go-nsq"
"github.com/redis/go-redis/v9"
"github.com/wenlng/go-captcha/v2/click"
"github.com/wenlng/go-captcha/v2/rotate"
"github.com/wenlng/go-captcha/v2/slide"
"github.com/zeromicro/go-zero/rest"
@@ -46,13 +47,14 @@ type ServiceContext struct {
WechatOfficial *officialAccount.OfficialAccount
RotateCaptcha rotate.Captcha
SlideCaptcha slide.Captcha
TextCaptcha click.Captcha
Sensitive *sensitive.Manager
StorageManager *manager.Manager
MinioClient *minio.Client
GeoRegionData *geo_json.RegionData
NSQProducer *nsq.Producer
ZincClient *zincx.ZincClient
XCipher *xcipher.XCipher
PN *pixelnebula.PixelNebula
}
func NewServiceContext(c config.Config) *ServiceContext {
@@ -72,6 +74,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
WechatOfficial: wechat_official.NewWechatPublic(c.Wechat.AppID, c.Wechat.AppSecret, c.Wechat.Token, c.Wechat.AESKey, c.Redis.Host, c.Redis.Pass, c.Redis.DB),
RotateCaptcha: initialize.NewRotateCaptcha(),
SlideCaptcha: initialize.NewSlideCaptcha(),
TextCaptcha: initialize.NewTextCaptcha(),
Sensitive: sensitivex.NewSensitive(),
StorageManager: storage.InitStorageManager(),
AiSvcRpc: aiservice.NewAiService(zrpc.MustNewClient(c.AiSvcRpc)),
@@ -79,7 +82,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
GeoRegionData: geo_json.NewGeoJSON(),
NSQProducer: nsqx.NewNsqProducer(c.NSQ.NSQDHost),
ZincClient: zincx.NewZincClient(c.Zinc.URL, c.Zinc.Username, c.Zinc.Password),
XCipher: xcipher.NewXCipher([]byte(c.Encrypt.Key)),
PN: pixelnebula.NewPixelNebula().WithDefaultCache(),
}
return serviceContext
}

View File

@@ -18,6 +18,13 @@ type AddImageToAlbumRequest struct {
Bucket string `json:"bucket"`
}
type AdminLoginRequest struct {
Account string `json:"account"`
Password string `json:"password"`
Dots string `json:"dots"`
Key string `json:"key"`
}
type Album struct {
ID int64 `json:"id"`
Name string `json:"name"`
@@ -175,6 +182,9 @@ type CoordinateMeta struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
ImageCount int64 `json:"image_count"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
}
type DeleteImageRequest struct {
@@ -325,6 +335,8 @@ type PrivateImageListRequest struct {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
Password string `json:"password"`
Dots string `json:"dots"`
Key string `json:"key"`
}
type QueryDeleteRecordRequest struct {
@@ -517,6 +529,13 @@ type SingleImageRequest struct {
ID int64 `json:"id"`
}
type SinglePrivateImageRequest struct {
ID int64 `json:"id"`
Password string `json:"password"`
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
type SlideCaptchaResponse struct {
Key string `json:"key"`
Image string `json:"image"`
@@ -572,6 +591,12 @@ type StroageNode struct {
Children []StorageMeta `json:"children"`
}
type TextCaptchaResponse struct {
Key string `json:"key"`
Image string `json:"image"`
Thumb string `json:"thumb"`
}
type ThingDetailListRequest struct {
TagName string `json:"tag_name"`
Provider string `json:"provider"`
@@ -609,6 +634,21 @@ type UploadRequest struct {
UserId string `json:"user_id"`
}
type UserInfoListResponse struct {
Records []UserMeta `json:"records"`
}
type UserMeta struct {
ID int64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Phone string `json:"phone"`
Status int64 `json:"status"`
CreatedAt string `json:"created_at"`
}
type UserSecuritySettingResponse struct {
BindPhone bool `json:"bind_phone,default=false"`
BindEmail bool `json:"bind_email,default=falsel"`
@@ -633,3 +673,8 @@ type WechatOffiaccountLoginRequest struct {
Openid string `json:"openid"`
ClientId string `json:"client_id"`
}
type ImageStreamResponse struct {
ContentType string `json:"content_type"`
Size int64 `json:"size"`
}

View File

@@ -15,7 +15,7 @@ passwordNotMatch = "password not match"
passwordFormatError = "password format error"
resetPasswordError = "reset password error"
loginSuccess = "login success"
notPermission = "not permission"
[sms]
smsSendTooFrequently = "sms send too frequently"
smsSendFailed = "sms send failed"

View File

@@ -15,6 +15,7 @@ passwordNotMatch = "两次输入的密码不一致!"
passwordFormatError = "密码格式错误!"
resetPasswordError = "重置密码失败!"
loginSuccess = "登录成功!"
notPermission = "无权访问!"
[sms]
smsSendTooFrequently = "验证码发送过于频繁,请稍后再试!"