1. 小程序端

2. 视频优化
This commit is contained in:
李琦
2026-07-30 14:11:52 +08:00
parent 4f4153ee0e
commit a9eb20f0e1
34 changed files with 9955 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
---
description:
alwaysApply: true
---
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面

283
server/handlers/wx_auth.go Normal file
View File

@@ -0,0 +1,283 @@
package handlers
import (
"crypto/rand"
"fmt"
"math/big"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/middleware"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
"gorm.io/gorm"
)
const guestCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func randomGuestSuffix(minLen, maxLen int) string {
nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(maxLen-minLen+1)))
n := int(nBig.Int64()) + minLen
b := make([]byte, n)
for i := 0; i < n; i++ {
idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(guestCharset))))
b[i] = guestCharset[idx.Int64()]
}
return string(b)
}
func uniqueGuestUsername() (string, error) {
for i := 0; i < 8; i++ {
name := "游客_" + randomGuestSuffix(6, 10)
exist, err := repositories.GetUserByUsername(name)
if err != nil {
return "", err
}
if exist == nil {
return name, nil
}
}
return "", fmt.Errorf("failed to allocate guest username")
}
// WxLogin 微信小程序静默登录 / 自动建游客账号
func WxLogin(c *gin.Context) {
var req models.WxLoginRequest
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Code) == "" {
utils.Error(c, 400, "Invalid request")
return
}
session, err := utils.Code2Session(req.Code)
if err != nil {
utils.Error(c, 400, err.Error())
return
}
user, err := repositories.GetUserByWxOpenID(session.OpenID)
if err != nil {
utils.ServerError(c, err)
return
}
if user == nil {
username, err := uniqueGuestUsername()
if err != nil {
utils.ServerError(c, err)
return
}
hash, err := utils.HashPassword(randomGuestSuffix(16, 24))
if err != nil {
utils.ServerError(c, err)
return
}
openidPrefix := session.OpenID
if len(openidPrefix) > 12 {
openidPrefix = openidPrefix[:12]
}
user = &models.User{
Username: username,
Email: fmt.Sprintf("wx_%s@guest.local", openidPrefix),
Avatar: "",
PasswordHash: hash,
WxOpenID: session.OpenID,
Role: "viewer",
IsActive: 1,
}
if role, rerr := repositories.GetRoleByName("viewer"); rerr == nil && role != nil {
user.RoleID = role.ID
}
if err := repositories.CreateUser(user); err != nil {
utils.ServerError(c, err)
return
}
// reload with role name
user, err = repositories.GetUserByWxOpenID(session.OpenID)
if err != nil || user == nil {
utils.ServerError(c, fmt.Errorf("created user but reload failed"))
return
}
}
if user.IsActive == 0 {
utils.Error(c, 403, "Account disabled")
return
}
tokenString, expireUnix, err := middleware.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
utils.ServerError(c, err)
return
}
go func() {
ip := c.ClientIP()
location := utils.GetRegion(ip)
logEntry := &models.UserAccessLog{
UserID: user.ID,
UserIP: ip,
UserLocation: location,
}
_ = repositories.CreateUserAccessLog(logEntry)
}()
utils.Success(c, gin.H{
"token": tokenString,
"expire": expireUnix,
"user": repositories.BuildUserResponse(user),
})
}
// isGuestUser 判断是否为可合并的游客账号viewer 或 游客_ 前缀)
func isGuestUser(u *models.User) bool {
if u == nil {
return false
}
role := strings.ToLower(strings.TrimSpace(u.Role))
if strings.HasPrefix(u.Username, "游客_") {
return true
}
return role == "viewer" || role == "guest"
}
// isStaffUser 判断是否为可绑定的员工账号
func isStaffUser(u *models.User) bool {
if u == nil {
return false
}
role := strings.ToLower(strings.TrimSpace(u.Role))
return role == "admin" || role == "editor" || strings.Contains(role, "admin") || strings.Contains(role, "editor")
}
// MergeAccountRequest 游客合并到管理员账号
type MergeAccountRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// MergeAccount 游客校验管理员密码后,将微信 openid 迁到管理员,并逻辑删除游客
func MergeAccount(c *gin.Context) {
var req MergeAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
guestIDVal, ok := c.Get("userID")
if !ok {
utils.Error(c, 401, "Unauthorized")
return
}
guestID, ok := guestIDVal.(uint)
if !ok {
utils.Error(c, 401, "Unauthorized")
return
}
guest, err := repositories.GetUserByID(guestID)
if err != nil {
utils.ServerError(c, err)
return
}
if guest == nil {
utils.Error(c, 401, "Unauthorized")
return
}
if !isGuestUser(guest) {
utils.Error(c, 400, "当前账号不是游客,无法合并")
return
}
openid := strings.TrimSpace(guest.WxOpenID)
if openid == "" {
utils.Error(c, 400, "当前游客未绑定微信,无法合并")
return
}
target, err := repositories.GetUserByUsername(strings.TrimSpace(req.Username))
if err != nil {
utils.ServerError(c, err)
return
}
if target == nil || !utils.CheckPasswordHash(req.Password, target.PasswordHash) {
utils.Error(c, 401, "Invalid username or password")
return
}
if target.IsActive == 0 {
utils.Error(c, 403, "Account disabled")
return
}
if !isStaffUser(target) {
utils.Error(c, 403, "目标账号不是管理员或编辑者")
return
}
if target.ID == guest.ID {
utils.Error(c, 400, "不能合并到当前账号")
return
}
existingOpenID := strings.TrimSpace(target.WxOpenID)
if existingOpenID != "" && existingOpenID != openid {
utils.Error(c, 409, "该管理员已绑定其他微信账号")
return
}
// 事务:清空游客 openid → 软删游客 → 绑定到管理员
err = config.DB.Transaction(func(tx *gorm.DB) error {
now := time.Now().Unix()
if err := tx.Model(&models.User{}).Where("id = ?", guest.ID).
Updates(map[string]interface{}{
"wx_openid": nil,
"updated_at": now,
}).Error; err != nil {
return err
}
if err := tx.Model(&models.User{}).Where("id = ?", guest.ID).
Update("deleted_at", now).Error; err != nil {
return err
}
if existingOpenID == openid {
return nil
}
return tx.Model(&models.User{}).
Where("id = ? AND deleted_at = ?", target.ID, 0).
Updates(map[string]interface{}{
"wx_openid": openid,
"updated_at": now,
}).Error
})
if err != nil {
utils.ServerError(c, err)
return
}
admin, err := repositories.GetUserByID(target.ID)
if err != nil || admin == nil {
utils.ServerError(c, fmt.Errorf("merged but reload admin failed"))
return
}
tokenString, expireUnix, err := middleware.GenerateToken(admin.ID, admin.Username, admin.Role)
if err != nil {
utils.ServerError(c, err)
return
}
go func() {
ip := c.ClientIP()
location := utils.GetRegion(ip)
logEntry := &models.UserAccessLog{
UserID: admin.ID,
UserIP: ip,
UserLocation: location,
}
_ = repositories.CreateUserAccessLog(logEntry)
}()
utils.Success(c, gin.H{
"token": tokenString,
"expire": expireUnix,
"user": repositories.BuildUserResponse(admin),
})
}

59
server/utils/wechat.go Normal file
View File

@@ -0,0 +1,59 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
type WxSessionResult struct {
OpenID string `json:"openid"`
SessionKey string `json:"session_key"`
UnionID string `json:"unionid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
// Code2Session exchanges mini-program login code for openid.
func Code2Session(code string) (*WxSessionResult, error) {
appID := os.Getenv("WECHAT_APP_ID")
secret := os.Getenv("WECHAT_APP_SECRET")
if appID == "" {
appID = "wx67e73fb3c9882dfc"
}
if secret == "" {
return nil, fmt.Errorf("WECHAT_APP_SECRET is not configured")
}
q := url.Values{}
q.Set("appid", appID)
q.Set("secret", secret)
q.Set("js_code", code)
q.Set("grant_type", "authorization_code")
endpoint := "https://api.weixin.qq.com/sns/jscode2session?" + q.Encode()
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result WxSessionResult
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if result.ErrCode != 0 || result.OpenID == "" {
return nil, fmt.Errorf("jscode2session failed: %d %s", result.ErrCode, result.ErrMsg)
}
return &result, nil
}