feat: 修改短信认证bug
This commit is contained in:
parent
aca45f7822
commit
e1829b2296
@ -20,6 +20,6 @@ REDIS_DB=0
|
||||
# 与 asset.env 中的阿里云 AccessKey 相同(同一账号)
|
||||
SMS_ACCESS_KEY_ID=LTAI5t6QcdJHpYbCPxM8SXYE
|
||||
SMS_ACCESS_KEY_SECRET=ybvjSEb7wilMt3qT5nOppYPoNVayCD
|
||||
SMS_SIGN_NAME=TopFans
|
||||
SMS_SIGN_NAME=上海顶粉数字科技
|
||||
SMS_TEMPLATE_CODE=SMS_314621237
|
||||
SMS_REGION=cn-hangzhou
|
||||
|
||||
@ -93,6 +93,18 @@ REDIS_DB="${REDIS_DB:-0}"
|
||||
DB_ARGS=(-db-host="$DB_HOST" -db-port="$DB_PORT" -db-user="$DB_USER" -db-password="$DB_PASSWORD" -db-name="$DB_NAME")
|
||||
REDIS_ARGS=(-redis-host="$REDIS_HOST" -redis-port="$REDIS_PORT" -redis-db="$REDIS_DB" -redis-password="$REDIS_PASSWORD")
|
||||
|
||||
# 加载服务私有 env(对齐 deploy/envs/ 部署路径)
|
||||
# 用法: load_service_env name -> source deploy/envs/<shortName>.env
|
||||
load_service_env() {
|
||||
local name=$1
|
||||
local service_env="$SCRIPT_DIR/deploy/envs/${name%Service}.env"
|
||||
if [ -f "$service_env" ]; then
|
||||
set -a
|
||||
source "$service_env"
|
||||
set +a
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动一个服务
|
||||
# 用法: start_service name binary port use_db use_redis
|
||||
start_service() {
|
||||
@ -104,6 +116,13 @@ start_service() {
|
||||
|
||||
echo -e "${GREEN}🚀 启动 $name...${NC}"
|
||||
|
||||
# Only services whose deploy/envs entry adds new vars not in
|
||||
# backend/.env need the per-service source. Loading the rest
|
||||
# would clobber dev's ports/JWT_SECRET/OSS keys with prod values.
|
||||
case "$name" in
|
||||
userService) load_service_env "$name" ;;
|
||||
esac
|
||||
|
||||
local args=("-port=$port")
|
||||
if [ "$use_db" = "1" ]; then
|
||||
args+=("${DB_ARGS[@]}")
|
||||
@ -210,6 +229,12 @@ restart_service() {
|
||||
# Step 3: 启动新进程
|
||||
sleep 1
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 重新加载服务私有 env(只有给 dev 引入新变量的服务才需要)
|
||||
case "$name" in
|
||||
userService) load_service_env "$name" ;;
|
||||
esac
|
||||
|
||||
local args=("-port=$port")
|
||||
if [ "$use_db" = "1" ]; then
|
||||
args+=("${DB_ARGS[@]}")
|
||||
|
||||
@ -895,6 +895,76 @@ func (ctrl *AssetController) GetOSSUploadSignature(c *gin.Context) {
|
||||
response.Success(c, policyToken)
|
||||
}
|
||||
|
||||
// GetPublicOSSUploadSignature 公开版 OSS 上传签名(用于注册等未登录场景的头像上传)
|
||||
// @Summary 获取公开 OSS 上传签名(注册流程用)
|
||||
// @Description 用于注册等未登录场景下上传头像,无需鉴权。
|
||||
// @Description 限定 key 必须以 avatar/register-pending/{key}/ 开头,key 由前端传入(如手机号),并要求 key 与 scene+key 一致。
|
||||
// @Tags assets
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param scene query string true "场景,目前固定为 register"
|
||||
// @Param key query string true "前端传入的命名空间(注册时传 mobile),仅允许 [a-zA-Z0-9_-],最长 32 字符"
|
||||
// @Success 200 {object} response.Response "成功返回上传签名信息"
|
||||
// @Failure 400 {object} response.Response "参数错误"
|
||||
// @Failure 500 {object} response.Response "OSS配置错误或生成签名失败"
|
||||
// @Router /api/v1/public/oss/upload-signature [get]
|
||||
func (ctrl *AssetController) GetPublicOSSUploadSignature(c *gin.Context) {
|
||||
scene := c.Query("scene")
|
||||
key := c.Query("key")
|
||||
if scene != "register" {
|
||||
response.Error(c, http.StatusBadRequest, "参数错误: scene 仅支持 register")
|
||||
return
|
||||
}
|
||||
if !isValidNamespaceKey(key) {
|
||||
response.Error(c, http.StatusBadRequest, "参数错误: key 仅允许字母数字下划线中划线,最长 32 字符")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := config.Load()
|
||||
if cfg.OSS.BucketName == "" || cfg.OSS.RoleArn == "" {
|
||||
response.Error(c, http.StatusInternalServerError, "OSS 配置未完成")
|
||||
return
|
||||
}
|
||||
|
||||
// 强制使用 avatar 目录,并将 key 限定到 avatar/register-pending/{key}/ 子目录
|
||||
uploadDir := fmt.Sprintf("%sregister-pending/%s/", cfg.OSS.AvatarDir, key)
|
||||
token, err := ctrl.generateOSSPolicyTokenWithDir(cfg.OSS, uploadDir)
|
||||
if err != nil {
|
||||
logger.Logger.Error("Generate public OSS signature failed",
|
||||
zap.Error(err),
|
||||
zap.String("scene", scene),
|
||||
zap.String("key", key),
|
||||
)
|
||||
response.Error(c, http.StatusInternalServerError, "生成签名失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.Info("Public OSS signature generated",
|
||||
zap.String("scene", scene),
|
||||
zap.String("key", key),
|
||||
)
|
||||
|
||||
response.Success(c, token)
|
||||
}
|
||||
|
||||
// isValidNamespaceKey 校验公开上传的命名空间 key(仅允许字母数字下划线中划线)
|
||||
func isValidNamespaceKey(key string) bool {
|
||||
if key == "" || len(key) > 32 {
|
||||
return false
|
||||
}
|
||||
for _, r := range key {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= 'A' && r <= 'Z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '_' || r == '-':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// generateOSSPolicyToken 生成 OSS 上传策略和签名
|
||||
func (ctrl *AssetController) generateOSSPolicyToken(
|
||||
ossConfig config.OSSConfig,
|
||||
@ -902,7 +972,22 @@ func (ctrl *AssetController) generateOSSPolicyToken(
|
||||
starID interface{},
|
||||
uploadType string,
|
||||
) (map[string]interface{}, error) {
|
||||
// 根据上传类型获取基础目录
|
||||
baseDir := ossConfig.GetUploadDir(uploadType)
|
||||
// 动态生成上传目录(基于用户ID和上传类型)
|
||||
uploadDir := baseDir
|
||||
if userID != nil && starID != nil {
|
||||
uploadDir = fmt.Sprintf("%s%d/%d/", baseDir, userID, starID)
|
||||
}
|
||||
return ctrl.generateOSSPolicyTokenWithDir(ossConfig, uploadDir)
|
||||
}
|
||||
|
||||
// generateOSSPolicyTokenWithDir 按显式目录生成 OSS 上传策略和签名
|
||||
// (用于公开场景等需要把 key 限定到非 userID/starID 命名空间的场景)
|
||||
func (ctrl *AssetController) generateOSSPolicyTokenWithDir(
|
||||
ossConfig config.OSSConfig,
|
||||
uploadDir string,
|
||||
) (map[string]interface{}, error) {
|
||||
// 1. 创建 STS 凭证提供器
|
||||
credConfig := new(credentials.Config).
|
||||
SetType("ram_role_arn").
|
||||
@ -929,15 +1014,6 @@ func (ctrl *AssetController) generateOSSPolicyToken(
|
||||
date := utcTime.Format("20060102")
|
||||
expiration := utcTime.Add(time.Duration(ossConfig.TokenExpireTime) * time.Second)
|
||||
|
||||
// 根据上传类型获取基础目录
|
||||
baseDir := ossConfig.GetUploadDir(uploadType)
|
||||
|
||||
// 动态生成上传目录(基于用户ID和上传类型)
|
||||
uploadDir := baseDir
|
||||
if userID != nil && starID != nil {
|
||||
uploadDir = fmt.Sprintf("%s%d/%d/", baseDir, userID, starID)
|
||||
}
|
||||
|
||||
// 构建 Policy 条件
|
||||
conditions := []interface{}{
|
||||
map[string]string{"bucket": ossConfig.BucketName},
|
||||
|
||||
@ -135,6 +135,12 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
|
||||
fanIdentities.GET("", userCtrl.GetFanIdentities) // 获取可选粉丝身份列表
|
||||
}
|
||||
|
||||
// 公开 OSS 上传签名(用于注册等未登录场景的上传,如注册头像)
|
||||
public := v1.Group("/public")
|
||||
{
|
||||
public.GET("/oss/upload-signature", assetCtrl.GetPublicOSSUploadSignature) // 公开 OSS 上传签名
|
||||
}
|
||||
|
||||
// 当前用户相关路由(需要认证)
|
||||
me := v1.Group("/me")
|
||||
me.Use(middleware.AuthMiddleware())
|
||||
|
||||
@ -390,6 +390,7 @@ type RegisterRequest struct {
|
||||
StarId int64 `protobuf:"varint,3,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 选择第一个粉丝身份的明星ID
|
||||
Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` // 第一个粉丝身份的昵称
|
||||
VerifyToken string `protobuf:"bytes,5,opt,name=verify_token,json=verifyToken,proto3" json:"verify_token,omitempty"` // 短信验证token(注册前必须先通过短信验证)
|
||||
AvatarUrl string `protobuf:"bytes,6,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` // 头像URL(可选;为空则后端使用默认头像)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -459,6 +460,13 @@ func (x *RegisterRequest) GetVerifyToken() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterRequest) GetAvatarUrl() string {
|
||||
if x != nil {
|
||||
return x.AvatarUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 注册响应
|
||||
type RegisterResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -3147,13 +3155,15 @@ const file_user_proto_rawDesc = "" +
|
||||
"\tis_active\x18\a \x01(\bR\bisActive\x12\x1d\n" +
|
||||
"\n" +
|
||||
"created_at\x18\b \x01(\x03R\tcreatedAt\x12\x10\n" +
|
||||
"\x03tag\x18\t \x01(\tR\x03tag\"\x9d\x01\n" +
|
||||
"\x03tag\x18\t \x01(\tR\x03tag\"\xbc\x01\n" +
|
||||
"\x0fRegisterRequest\x12\x16\n" +
|
||||
"\x06mobile\x18\x01 \x01(\tR\x06mobile\x12\x1a\n" +
|
||||
"\bpassword\x18\x02 \x01(\tR\bpassword\x12\x17\n" +
|
||||
"\astar_id\x18\x03 \x01(\x03R\x06starId\x12\x1a\n" +
|
||||
"\bnickname\x18\x04 \x01(\tR\bnickname\x12!\n" +
|
||||
"\fverify_token\x18\x05 \x01(\tR\vverifyToken\"\xe9\x01\n" +
|
||||
"\fverify_token\x18\x05 \x01(\tR\vverifyToken\x12\x1d\n" +
|
||||
"\n" +
|
||||
"avatar_url\x18\x06 \x01(\tR\tavatarUrl\"\xe9\x01\n" +
|
||||
"\x10RegisterResponse\x120\n" +
|
||||
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12!\n" +
|
||||
"\faccess_token\x18\x02 \x01(\tR\vaccessToken\x12\x1d\n" +
|
||||
|
||||
@ -61,6 +61,7 @@ message RegisterRequest {
|
||||
int64 star_id = 3; // 选择第一个粉丝身份的明星ID
|
||||
string nickname = 4; // 第一个粉丝身份的昵称
|
||||
string verify_token = 5; // 短信验证token(注册前必须先通过短信验证)
|
||||
string avatar_url = 6; // 头像URL(可选;为空则后端使用默认头像)
|
||||
}
|
||||
|
||||
// 注册响应
|
||||
|
||||
@ -68,7 +68,49 @@ apt-get install protobuf-compiler
|
||||
yum install protobuf-compiler
|
||||
```
|
||||
|
||||
### 4. 生成代码
|
||||
### 4. 环境变量配置 ⚠️
|
||||
|
||||
**本项目有两套互相独立的配置文件,设计上是"二选一"关系,不要混用:**
|
||||
|
||||
| 文件 | 用途 | 何时生效 |
|
||||
| --- | --- | --- |
|
||||
| `backend/.env` | **Dev(本机开发)** 配置 | `./backend/dev.sh` 自动 source |
|
||||
| `backend/deploy/envs/*.env` | **生产多机部署** 配置(每个服务一份) | 仅在目标服务器上由 systemd / docker / 部署脚本 source |
|
||||
|
||||
**为什么不能混用** — 同一个变量在两套里值不同,后者会覆盖前者:
|
||||
|
||||
| 变量 | `backend/.env`(dev) | `deploy/envs/gateway.env`(生产) | 混用后果 |
|
||||
| --- | --- | --- | --- |
|
||||
| `JWT_SECRET` | `topfans-secret-key-please-change-in-production` | `your_secure_jwt_secret_here` | 所有 token 校验失败,需要登录的接口 401 |
|
||||
| `DUBBO_GALLERY_SERVICE_URL` | `tri://127.0.0.1:20004` | `tri://localhost:20001` | 调用 gallery 服务的接口连接失败 |
|
||||
| `DUBBO_ACTIVITY_SERVICE_URL` | `tri://127.0.0.1:20005` | `tri://localhost:20004` | 调用 activity 服务的接口连接失败 |
|
||||
| `OSS_ACCESS_KEY_ID` | `LTAI5t99tafzfyrzbbEbjryH` | `LTAI5t6QcdJHpYbCPxM8SXYE` | OSS 上传/下载 403 |
|
||||
| `DB_USER` / `DB_PASSWORD` | `postgres` / `123456` | `haihuizhu` / `admin` | 启动期就连不上 DB |
|
||||
| `ENV` | `development` | `production` | 日志格式、限流策略等会按生产行为跑 |
|
||||
|
||||
**dev.sh 当前的加载规则**(见 `dev.sh:load_service_env`):
|
||||
|
||||
- `dev.sh` 启动时**只** source `backend/.env`
|
||||
- 启动每个服务前,按服务名(去掉 `Service` 后缀)source 对应的 `deploy/envs/<name>.env`,**但仅对 `userService`** 生效
|
||||
- `userService` 之所以特殊:它的 `deploy/envs/user.env` 里有 `SMS_ACCESS_KEY_ID` / `SMS_ACCESS_KEY_SECRET` 等 `backend/.env` 没有的变量,需要叠加
|
||||
- **其他服务不再 source `deploy/envs/`** — 否则会被生产端口/密钥覆盖 dev 端口/密钥,导致接口大面积失败(典型症状:token 401、Dubbo 调用超时、OSS 403)
|
||||
|
||||
**生产部署时**(由部署脚本负责,与 dev.sh 无关):
|
||||
|
||||
- 把对应服务的 `deploy/envs/<name>.env` 放到目标服务器的 `/etc/topfans/<name>.env`
|
||||
- systemd unit / docker-compose 引用该文件
|
||||
- **不要** source `backend/.env`
|
||||
|
||||
**新增配置项时的纪律:**
|
||||
|
||||
1. 如果是 dev 和生产都需要的(如 `DB_*`、Dubbo URL 模板),在 `backend/.env` 改 + 在 `deploy/envs/*.env` 同步改
|
||||
2. 如果仅生产需要(如 `SMS_*`、OSS 角色),只改 `deploy/envs/<name>.env`
|
||||
3. 如果仅 dev 需要(如本地 mock 开关),只改 `backend/.env`
|
||||
4. 改完后,在 dev 环境跑一次 `./dev.sh` 验证没有端口/密钥错乱
|
||||
|
||||
---
|
||||
|
||||
### 5. 生成代码
|
||||
|
||||
```bash
|
||||
# 生成 gRPC 代码
|
||||
@ -81,6 +123,24 @@ make swagger
|
||||
make all
|
||||
```
|
||||
|
||||
### 6. 启动服务
|
||||
|
||||
```bash
|
||||
# 一键启动所有服务(带文件热更新)
|
||||
./dev.sh
|
||||
|
||||
# 或者只跑某个服务(略)
|
||||
# 详见 dev.sh 注释
|
||||
```
|
||||
|
||||
启动后会监听:
|
||||
|
||||
- Gateway: http://localhost:8080
|
||||
- UserService: tri://localhost:20000
|
||||
- Swagger UI: http://localhost:8080/swagger/index.html
|
||||
|
||||
> ⚠️ 启动前确认 `backend/.env` 里的 `DB_HOST/REDIS_HOST` 在本机能通(默认 `localhost`)。如果用远程 DB/Redis,改 `backend/.env` 而不是 `deploy/envs/*.env`。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
-- Drop the orphan reverse FK on fan_profiles.user_id.
|
||||
-- fan_profiles already has a CASCADE FK fk_fan_profiles_user on the same
|
||||
-- (user_id) column. The duplicate fk_users_fan_profiles (declared without
|
||||
-- ON DELETE CASCADE) blocks hard-deletes of users and breaks the soft-delete
|
||||
-- re-registration path.
|
||||
|
||||
ALTER TABLE fan_profiles DROP CONSTRAINT IF EXISTS fk_users_fan_profiles;
|
||||
@ -4,7 +4,9 @@ go 1.25.5
|
||||
|
||||
require (
|
||||
dubbo.apache.org/dubbo-go/v3 v3.3.1
|
||||
github.com/alibabacloud-go/dysmsapi-20180501/v2 v2.0.8
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.5.1
|
||||
github.com/alibabacloud-go/tea v1.3.13
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
github.com/topfans/backend v0.0.0
|
||||
go.uber.org/zap v1.27.1
|
||||
@ -19,9 +21,7 @@ require (
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect
|
||||
github.com/alibaba/sentinel-golang v1.0.4 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.3.13 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect
|
||||
|
||||
@ -79,7 +79,7 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 h1:LHhjxZkNWAKWepxcWyzgFgo0X6TUVhL7sC7ANc60p8A=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
@ -90,8 +90,8 @@ github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6p
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/dysmsapi-20180501/v2 v2.0.8 h1:aDPyz6C+nenypx24N5qEt09NjpS6mu7Cu1A+wf9UTaY=
|
||||
github.com/alibabacloud-go/dysmsapi-20180501/v2 v2.0.8/go.mod h1:e/vWJ5gLVnraPROSh+3oMSodf5ukaUlqNgH0IIcnz98=
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.5.1 h1:CyJ1adk5jlg7acrbG1sgdZ+EXTZNZHwhNQAf6VFfySo=
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.5.1/go.mod h1:J1zab9/VxVJGdZ5pSK/BbUot7CkaSkRXdaLKAXXRLoY=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
|
||||
@ -150,6 +150,12 @@ func (s *authService) Register(ctx context.Context, req *pb.RegisterRequest) (*p
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// 如果前端传入了头像URL则使用,否则保持空(前端会基于userId渲染默认头像)
|
||||
if req.AvatarUrl != "" {
|
||||
avatarURL := req.AvatarUrl
|
||||
user.AvatarURL = &avatarURL
|
||||
}
|
||||
|
||||
// 手动加密密码(因为需要在事务中使用)
|
||||
hashedPassword, err := repository.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20180501/v2/client"
|
||||
dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v5/client"
|
||||
"github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/topfans/backend/pkg/logger"
|
||||
@ -22,15 +22,17 @@ var smsClient *dysmsapi20170525.Client
|
||||
func InitSMSClient() error {
|
||||
cfg := config.GetSMSConfig()
|
||||
|
||||
// If credentials are not set, skip initialization
|
||||
// Fail fast when credentials are missing. Returning nil here would let
|
||||
// the service come up with a nil smsClient, which SendVerificationCode
|
||||
// silently skips — making send-code return 200 without delivering any SMS.
|
||||
if cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" {
|
||||
logger.Logger.Warn("SMS credentials not configured, SMS client not initialized")
|
||||
return nil
|
||||
return errors.New("SMS credentials missing: SMS_ACCESS_KEY_ID and SMS_ACCESS_KEY_SECRET must be set")
|
||||
}
|
||||
|
||||
openapiConfig := &client.Config{
|
||||
AccessKeyId: tea.String(cfg.AccessKeyID),
|
||||
AccessKeySecret: tea.String(cfg.AccessKeySecret),
|
||||
RegionId: tea.String(cfg.Region),
|
||||
}
|
||||
openapiConfig.Endpoint = tea.String("dysmsapi.aliyuncs.com")
|
||||
|
||||
@ -130,27 +132,39 @@ func SendVerificationCode(ctx context.Context, mobile, ip string) (int, error) {
|
||||
return 0, fmt.Errorf("failed to generate code: %w", err)
|
||||
}
|
||||
|
||||
// Step 6: Call Aliyun SMS API to send (if client is initialized)
|
||||
if smsClient != nil {
|
||||
cfg := config.GetSMSConfig()
|
||||
sendReq := &dysmsapi20170525.SendMessageWithTemplateRequest{
|
||||
To: tea.String("86" + mobile), // China area code + mobile
|
||||
From: tea.String(cfg.SignName),
|
||||
TemplateCode: tea.String(cfg.TemplateCode),
|
||||
TemplateParam: tea.String(fmt.Sprintf(`{"code":"%s"}`, code)),
|
||||
}
|
||||
|
||||
_, err := smsClient.SendMessageWithTemplate(sendReq)
|
||||
if err != nil {
|
||||
logger.Error("Failed to send SMS via Aliyun", zap.Error(err))
|
||||
return 0, fmt.Errorf("failed to send SMS: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("SMS sent successfully via Aliyun")
|
||||
} else {
|
||||
logger.Warn("SMS client not initialized, skipping actual SMS send")
|
||||
// Step 6: Call Aliyun SMS API to send. smsClient==nil is a programming
|
||||
// error (InitSMSClient should have failed-fast at startup); surface it
|
||||
// as 5xx instead of pretending success.
|
||||
if smsClient == nil {
|
||||
return 0, errors.New("SMS client not initialized")
|
||||
}
|
||||
|
||||
cfg := config.GetSMSConfig()
|
||||
sendReq := &dysmsapi20170525.SendSmsRequest{
|
||||
PhoneNumbers: tea.String(mobile),
|
||||
SignName: tea.String(cfg.SignName),
|
||||
TemplateCode: tea.String(cfg.TemplateCode),
|
||||
TemplateParam: tea.String(fmt.Sprintf(`{"code":"%s"}`, code)),
|
||||
}
|
||||
|
||||
resp, err := smsClient.SendSms(sendReq)
|
||||
if err != nil {
|
||||
logger.Error("Failed to send SMS via Aliyun", zap.Error(err))
|
||||
return 0, fmt.Errorf("failed to send SMS: %w", err)
|
||||
}
|
||||
if resp == nil || resp.Body == nil {
|
||||
return 0, errors.New("Aliyun SMS response empty")
|
||||
}
|
||||
if resp.Body.Code != nil && *resp.Body.Code != "OK" {
|
||||
logger.Error("Aliyun SMS business error",
|
||||
zap.String("code", tea.StringValue(resp.Body.Code)),
|
||||
zap.String("message", tea.StringValue(resp.Body.Message)),
|
||||
zap.String("request_id", tea.StringValue(resp.Body.RequestId)))
|
||||
return 0, fmt.Errorf("Aliyun SMS error: code=%s, message=%s",
|
||||
tea.StringValue(resp.Body.Code), tea.StringValue(resp.Body.Message))
|
||||
}
|
||||
logger.Info("SMS sent successfully via Aliyun")
|
||||
|
||||
// Step 7: Save code to Redis with 60s TTL
|
||||
err = SaveSMSCode(ctx, mobile, code, 60*time.Second)
|
||||
if err != nil {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"appid" : "__UNI__F199FF4",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.5",
|
||||
"versionCode" : 103,
|
||||
"versionCode" : 105,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
||||
@ -1199,6 +1199,7 @@ onUnmounted(() => {
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.countdown-val {
|
||||
|
||||
@ -570,12 +570,14 @@ defineExpose({
|
||||
|
||||
.task-text-label {
|
||||
font-weight: 500;
|
||||
font-size: 12rpx;
|
||||
line-height: 100%;
|
||||
letter-spacing: 0%;
|
||||
font-size: 24rpx;
|
||||
/* line-height: 100%;
|
||||
letter-spacing: 0%; */
|
||||
color: #fff9e7;
|
||||
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.84);
|
||||
margin-top: 32rpx;
|
||||
white-space: nowrap;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
/* 水晶余额组件 */
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<!-- 背景图片 -->
|
||||
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
|
||||
<view class="background-overlay"></view>
|
||||
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper">
|
||||
<!-- 导航栏 -->
|
||||
@ -11,33 +11,36 @@
|
||||
<view class="nav-back" @click="goBack">
|
||||
<text class="back-icon">←</text>
|
||||
</view>
|
||||
<text class="nav-title">设置昵称</text>
|
||||
<text class="nav-title">设置头像与昵称</text>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 表单区域(居中部分) -->
|
||||
<view class="form-container">
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-wrapper">
|
||||
<Avatar :nickname="nickname" :size="200" :borderWidth="8" />
|
||||
<!-- 头像(点击可上传) -->
|
||||
<view class="avatar-wrapper" @tap="handleAvatarClick">
|
||||
<Avatar :nickname="nickname" :size="200" :borderWidth="8" :avatarUrl="userAvatarUrl" />
|
||||
<view class="avatar-edit-badge">
|
||||
<text class="avatar-edit-icon">✎</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 昵称输入框 -->
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
v-model="nickname"
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
v-model="nickname"
|
||||
placeholder="请输入昵称"
|
||||
maxlength="20"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view v-if="errorMessage" class="error-message">
|
||||
<text>{{ errorMessage }}</text>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 下一步按钮(圆形箭头) -->
|
||||
<view class="next-button-wrapper">
|
||||
<button class="btn-next" @click="handleNext">
|
||||
@ -46,6 +49,29 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像上传弹窗 -->
|
||||
<view class="avatar-modal" v-if="showAvatarModal" @tap="closeAvatarModal">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-title">设置头像</view>
|
||||
|
||||
<!-- 头像预览 -->
|
||||
<view class="avatar-preview">
|
||||
<Avatar :nickname="nickname" :size="180" :borderWidth="6" :avatarUrl="userAvatarUrl" />
|
||||
</view>
|
||||
|
||||
<!-- 上传按钮 -->
|
||||
<button class="upload-avatar-btn" @tap="handleUploadAvatar" :disabled="uploadingAvatar">
|
||||
{{ uploadingAvatar ? '上传中...' : '上传头像' }}
|
||||
</button>
|
||||
|
||||
<view class="upload-hint">支持JPG、PNG格式,大小不超过10MB</view>
|
||||
|
||||
<view class="modal-buttons">
|
||||
<button class="modal-btn-cancel" @tap="closeAvatarModal">取消</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -53,7 +79,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import Avatar from '../components/Avatar.vue';
|
||||
import { checkNicknameApi } from '@/utils/api.js';
|
||||
import { checkNicknameApi, getPublicOssSignatureApi } from '@/utils/api.js';
|
||||
import { validateNickname } from '@/utils/validator.js';
|
||||
|
||||
const store = useStore();
|
||||
@ -63,6 +89,11 @@ const nickname = ref('');
|
||||
const errorMessage = ref('');
|
||||
const isChecking = ref(false);
|
||||
|
||||
// 头像上传相关
|
||||
const userAvatarUrl = ref('');
|
||||
const showAvatarModal = ref(false);
|
||||
const uploadingAvatar = ref(false);
|
||||
|
||||
const goToAuthPage = () => {
|
||||
const hasRegisterDraft = Boolean(uni.getStorageSync('temp_register_mobile'));
|
||||
const authPageUrl = hasRegisterDraft ? '/pages/register/register' : '/pages/login/login';
|
||||
@ -87,6 +118,122 @@ const goBack = () => {
|
||||
goToAuthPage();
|
||||
};
|
||||
|
||||
// 打开头像上传弹窗
|
||||
const handleAvatarClick = () => {
|
||||
showAvatarModal.value = true;
|
||||
};
|
||||
|
||||
// 关闭头像上传弹窗
|
||||
const closeAvatarModal = () => {
|
||||
if (uploadingAvatar.value) return;
|
||||
showAvatarModal.value = false;
|
||||
};
|
||||
|
||||
// 选择并上传头像
|
||||
const handleUploadAvatar = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
|
||||
uni.getFileInfo({
|
||||
filePath: tempFilePath,
|
||||
success: async (fileInfo) => {
|
||||
if (fileInfo.size > 10 * 1024 * 1024) {
|
||||
uni.showToast({
|
||||
title: '图片大小不能超过10MB',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
await uploadAvatarToOss(tempFilePath);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('获取文件信息失败:', error);
|
||||
uni.showToast({
|
||||
title: '获取文件信息失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('选择图片失败:', error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 通过公开 OSS 签名接口上传头像
|
||||
const uploadAvatarToOss = async (filePath) => {
|
||||
try {
|
||||
uploadingAvatar.value = true;
|
||||
uni.showLoading({ title: '上传中...', mask: true });
|
||||
|
||||
// 1. 命名空间:使用注册手机号,便于注册成功后定位/迁移
|
||||
const mobile = uni.getStorageSync('temp_register_mobile') || 'anon';
|
||||
const signRes = await getPublicOssSignatureApi('register', mobile);
|
||||
if (signRes.code !== 200) {
|
||||
throw new Error(signRes.message || '获取签名失败');
|
||||
}
|
||||
|
||||
// 2. 上传到 OSS(文件名固定 avatar.png,避免签名 policy 范围外)
|
||||
uni.uploadFile({
|
||||
url: signRes.data.host,
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: {
|
||||
key: signRes.data.dir + 'avatar.png',
|
||||
policy: signRes.data.policy,
|
||||
success_action_status: '200',
|
||||
'x-oss-credential': signRes.data.x_oss_credential,
|
||||
'x-oss-date': signRes.data.x_oss_date,
|
||||
'x-oss-security-token': signRes.data.security_token,
|
||||
'x-oss-signature': signRes.data.signature,
|
||||
'x-oss-signature-version': signRes.data.x_oss_signature_version
|
||||
},
|
||||
success: (uploadRes) => {
|
||||
uni.hideLoading();
|
||||
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
|
||||
userAvatarUrl.value = `${signRes.data.host}/${signRes.data.dir}avatar.png`;
|
||||
uni.showToast({
|
||||
title: '头像已选择',
|
||||
icon: 'success'
|
||||
});
|
||||
closeAvatarModal();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: `上传失败,状态码: ${uploadRes.statusCode}`,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
uploadingAvatar.value = false;
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('OSS上传失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '上传失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
uploadingAvatar.value = false;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('上传头像失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || '上传失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
uploadingAvatar.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 下一步
|
||||
const handleNext = async () => {
|
||||
// 验证昵称
|
||||
@ -108,8 +255,6 @@ const handleNext = async () => {
|
||||
isChecking.value = true;
|
||||
|
||||
try {
|
||||
const trimmedNickname = nickname.value.trim();
|
||||
|
||||
// 检查昵称是否已被注册
|
||||
const res = await checkNicknameApi(trimmedNickname);
|
||||
if (res.data.exists === true) {
|
||||
@ -125,21 +270,12 @@ const handleNext = async () => {
|
||||
// 暂存昵称到临时存储
|
||||
uni.setStorageSync('temp_register_nickname', trimmedNickname);
|
||||
|
||||
// // 跳转到欢迎动画页面
|
||||
// uni.navigateTo({
|
||||
// url: '/pages/welcome/welcome'
|
||||
// });
|
||||
|
||||
// // 跳转到粉丝身份选择页面
|
||||
// uni.navigateTo({
|
||||
// url: '/pages/profile/selectRole'
|
||||
// });
|
||||
|
||||
// 获取临时存储的注册信息
|
||||
const mobile = uni.getStorageSync('temp_register_mobile');
|
||||
const password = uni.getStorageSync('temp_register_password');
|
||||
const star_id = 87; // 默认身份
|
||||
const verify_token = uni.getStorageSync('temp_register_verify_token') || '';
|
||||
const avatar_url = userAvatarUrl.value || '';
|
||||
|
||||
// 验证数据完整性
|
||||
if (!mobile || !password || !trimmedNickname || !star_id) {
|
||||
@ -172,7 +308,8 @@ const handleNext = async () => {
|
||||
password,
|
||||
star_id,
|
||||
nickname: trimmedNickname,
|
||||
verify_token
|
||||
verify_token,
|
||||
avatar_url
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
@ -232,7 +369,6 @@ const handleNext = async () => {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* filter: blur(20rpx); */
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@ -301,6 +437,7 @@ const handleNext = async () => {
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 80rpx;
|
||||
display: flex;
|
||||
@ -308,6 +445,28 @@ const handleNext = async () => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-edit-badge {
|
||||
position: absolute;
|
||||
right: 30%;
|
||||
bottom: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
border: 4rpx solid #fff;
|
||||
}
|
||||
|
||||
.avatar-edit-icon {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
@ -381,5 +540,96 @@ const handleNext = async () => {
|
||||
color: #e6e6e6;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 头像上传弹窗 */
|
||||
.avatar-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 80%;
|
||||
max-width: 600rpx;
|
||||
background-image: url('/static/starbookcontent/beijing.png');
|
||||
background-size: cover;
|
||||
background-position: center bottom;
|
||||
border-radius: 30rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.upload-avatar-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
border-radius: 44rpx;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.upload-avatar-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-avatar-btn:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.modal-btn-cancel {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
background: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.modal-btn-cancel::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn-cancel:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -336,11 +336,11 @@ onUnmounted(() => {
|
||||
|
||||
/* 第一排:3个大图突出显示 */
|
||||
.grid-card:nth-child(-n + 3) {
|
||||
width: calc(33.333% - 12rpx);
|
||||
width: calc(33% - 18rpx);
|
||||
}
|
||||
|
||||
.grid-card:nth-child(-n + 3) .card-image {
|
||||
height: 236rpx;
|
||||
height: 252rpx;
|
||||
box-shadow: 3px 3px 4.5px 2px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(0px);
|
||||
padding: 8rpx;
|
||||
@ -382,7 +382,9 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.corner-decoration.top-corner-decoration {
|
||||
left: -24rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
left: -16rpx;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -119,9 +119,9 @@ const actions = {
|
||||
},
|
||||
|
||||
// 注册
|
||||
async register({ commit }, { mobile, password, star_id, nickname, verify_token = '' }) {
|
||||
async register({ commit }, { mobile, password, star_id, nickname, verify_token = '', avatar_url = '' }) {
|
||||
try {
|
||||
const res = await registerApi(mobile, password, star_id, nickname, verify_token)
|
||||
const res = await registerApi(mobile, password, star_id, nickname, verify_token, avatar_url)
|
||||
if (res.code === 200 && res.data) {
|
||||
// 缓存 access_token
|
||||
const accessToken = res.data.access_token
|
||||
|
||||
@ -185,7 +185,7 @@ export function checkmobileApi(mobile) {
|
||||
}
|
||||
|
||||
// 注册接口
|
||||
export function registerApi(mobile, password, star_id, nickname, verify_token = '') {
|
||||
export function registerApi(mobile, password, star_id, nickname, verify_token = '', avatar_url = '') {
|
||||
return request({
|
||||
url: '/api/v1/auth/register',
|
||||
method: 'POST',
|
||||
@ -194,7 +194,8 @@ export function registerApi(mobile, password, star_id, nickname, verify_token =
|
||||
password,
|
||||
star_id,
|
||||
nickname,
|
||||
verify_token
|
||||
verify_token,
|
||||
avatar_url
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -508,6 +509,19 @@ export function getOssSignatureApi(type = 'asset', orderId = '') {
|
||||
/** 兼容旧调用名:与 getOssSignatureApi 相同 */
|
||||
export const getOssUploadSignatureApi = getOssSignatureApi
|
||||
|
||||
/**
|
||||
* 获取公开 OSS 上传签名(注册等未登录场景用)
|
||||
* 对应网关:GET /api/v1/public/oss/upload-signature
|
||||
* @param {string} scene 场景,目前固定为 'register'
|
||||
* @param {string} key 命名空间,注册场景下传 mobile(用于服务端限定上传目录)
|
||||
*/
|
||||
export function getPublicOssSignatureApi(scene, key) {
|
||||
return request({
|
||||
url: `/api/v1/public/oss/upload-signature?scene=${encodeURIComponent(scene)}&key=${encodeURIComponent(key)}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OSS 预签名 GET URL(私有桶读图)
|
||||
* 对应网关:GET /api/v1/assets/oss/presigned-url
|
||||
|
||||
Loading…
Reference in New Issue
Block a user