feat: update

This commit is contained in:
Rogee
2025-04-30 18:50:11 +08:00
parent 42c1c17c0a
commit 11288471d4
8 changed files with 204 additions and 91 deletions

View File

@@ -1,10 +1,14 @@
package http
import (
"time"
"quyun/app/models"
"quyun/database/schemas/public/model"
"quyun/providers/wechat"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/log"
)
// @provider
@@ -17,5 +21,31 @@ type wechats struct {
// @Bind url query
// @Bind user local
func (ctl *wechats) GetJsSDK(ctx fiber.Ctx, url string, user *model.Users) (*wechat.JsSDK, error) {
if user.AuthToken.Data.StableExpiresAt.After(time.Now()) {
token, err := ctl.wechat.RefreshAccessToken(user.AuthToken.Data.RefreshToken)
if err != nil {
return nil, err
}
log.Infof("refresh token: %+v", token)
stableToken, err := ctl.wechat.GetStableAccessToken()
if err != nil {
return nil, err
}
user.AuthToken.Data.StableAccessToken = stableToken.AccessToken
oldToken := user.AuthToken.Data
oldToken.AccessToken = token.AccessToken
oldToken.ExpiresAt = time.Now().Add(time.Second * time.Duration(token.ExpiresIn))
oldToken.StableAccessToken = stableToken.AccessToken
oldToken.StableExpiresAt = time.Now().Add(time.Second * time.Duration(stableToken.ExpiresIn))
if err := models.Users.UpdateUserToken(ctx.Context(), user.ID, oldToken); err != nil {
return nil, err
}
user.AuthToken.Data = oldToken
}
return ctl.wechat.GetJsSDK(user.AuthToken.Data.StableAccessToken, url)
}

View File

@@ -3,6 +3,7 @@ package middlewares
import (
"net/url"
"strings"
"time"
"quyun/app/models"
"quyun/pkg/utils"
@@ -70,6 +71,17 @@ func (f *Middlewares) Auth(ctx fiber.Ctx) error {
}
return ctx.Redirect().To(fullUrl)
}
// TOKEN 过期
if user.AuthToken.Data.ExpiresAt.Before(time.Now()) {
// remove cookie
ctx.ClearCookie("token")
if ctx.XHR() {
return ctx.SendStatus(fiber.StatusUnauthorized)
}
return ctx.Redirect().To(fullUrl)
}
ctx.Locals("user", user)
return ctx.Next()

View File

@@ -5,6 +5,7 @@ import (
"time"
"quyun/app/requests"
"quyun/database/fields"
"quyun/database/schemas/public/model"
"quyun/database/schemas/public/table"
@@ -385,3 +386,21 @@ func (m *usersModel) UpdateUsername(ctx context.Context, id int64, username stri
}
return nil
}
// UpdateUserToken
func (m *usersModel) UpdateUserToken(ctx context.Context, id int64, token fields.UserAuthToken) error {
tbl := table.Users
stmt := tbl.
UPDATE(tbl.AuthToken).
SET(fields.ToJson(token)).
WHERE(
tbl.ID.EQ(Int64(id)),
)
m.log.Infof("sql: %s", stmt.DebugSql())
if _, err := stmt.ExecContext(ctx, db); err != nil {
m.log.Errorf("error updating user token: %v", err)
return err
}
return nil
}

View File

@@ -11,6 +11,7 @@ func (r *ErrorResponse) Error() error {
type AccessTokenResponse struct {
ErrorResponse
AccessToken string `json:"access_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"` // seconds
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"` // seconds
}

View File

@@ -89,6 +89,26 @@ func (we *Client) wrapParams(params map[string]string) map[string]string {
return params
}
// RefreshAccessToken
func (we *Client) RefreshAccessToken(refreshToken string) (*AccessTokenResponse, error) {
params := we.wrapParams(map[string]string{
"grant_type": "refresh_token",
"refresh_token": refreshToken,
})
var data AccessTokenResponse
_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/refresh_token")
if err != nil {
return nil, errors.Wrap(err, "call /sns/oauth2/refresh_token failed")
}
if data.ErrCode != 0 {
return nil, data.Error()
}
return &data, nil
}
func (we *Client) GetAccessToken() (*AccessTokenResponse, error) {
params := map[string]string{
"grant_type": "client_credential",

View File

@@ -16,8 +16,8 @@ export function useWxSDK() {
"chooseImage",
"uploadImage",
"previewImage",
"onMenuShareTimeline",
"onMenuShareAppMessage",
"updateAppMessageShareData",
"updateAppMessageShareData",
"chooseWXPay",
],
openTagList: [],
@@ -35,20 +35,23 @@ export function useWxSDK() {
onSuccess = () => { },
onCancel = () => { }
) {
wx.onMenuShareTimeline({
console.log("setShareInfo called", shareInfo);
wx.updateTimelineShareData({
title: shareInfo.title, // 分享标题
link: shareInfo.link, // 分享链接可以不是当前页面该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: shareInfo.imgUrl,
success: function () {
console.log("分享朋友圈成功", e);
// 用户确认分享后执行的回调函数
onSuccess();
},
cancel: function () {
console.log("分享朋友圈取消", e);
onCancel();
// 用户取消分享后执行的回调函数
},
});
wx.onMenuShareAppMessage({
wx.updateAppMessageShareData({
title: shareInfo.title, // 分享标题
desc: shareInfo.desc,
link: shareInfo.link, // 分享链接可以不是当前页面该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
@@ -56,13 +59,13 @@ export function useWxSDK() {
type: "link", // 分享类型,music、video或link不填默认为link
success: function (e) {
// 用户确认分享后执行的回调函数
onSuccess();
console.log("分享成功", e);
onSuccess();
},
cancel: function (e) {
// 用户取消分享后执行的回调函数
onCancel();
console.log("分享取消", e);
onCancel();
},
});
}

View File

@@ -1,131 +1,159 @@
<script setup>
import Plyr from 'plyr'
import 'plyr/dist/plyr.css'
import { onMounted, onUnmounted, ref } from 'vue'
import { BsChevronLeft } from 'vue-icons-plus/bs'
import { useRoute, useRouter } from 'vue-router'
import { postApi } from '../api/postApi'
import { wechatApi } from '../api/wechatApi'
import { useWxSDK } from '../hooks/useWxSDK'
import Plyr from "plyr";
import "plyr/dist/plyr.css";
import { onMounted, onUnmounted, ref } from "vue";
import { BsChevronLeft } from "vue-icons-plus/bs";
import { useRoute, useRouter } from "vue-router";
import { postApi } from "../api/postApi";
import { wechatApi } from "../api/wechatApi";
import { useWxSDK } from "../hooks/useWxSDK";
const wx = useWxSDK()
const route = useRoute()
const router = useRouter()
const article = ref(null)
const buying = ref(false)
const player = ref(null)
const videoElement = ref(null)
const wx = useWxSDK();
const route = useRoute();
const router = useRouter();
const article = ref(null);
const buying = ref(false);
const player = ref(null);
const videoElement = ref(null);
const mediaLoaded = ref(false)
const mediaLoaded = ref(false);
const initializePlayer = () => {
if (videoElement.value) {
player.value = new Plyr(videoElement.value, {
controls: ['play', 'progress', 'current-time', 'duration', 'settings', 'fullscreen'],
settings: ['speed'],
controls: [
"play",
"progress",
"current-time",
"duration",
"settings",
"fullscreen",
],
settings: ["speed"],
speed: { selected: 1, options: [0.5, 0.75, 1] },
autoplay: false
})
autoplay: false,
});
player.value.on('play', async () => {
player.value.on("play", async () => {
if (!mediaLoaded.value) {
await loadVideoSource()
await loadVideoSource();
}
})
});
player.value.on('ended', () => {
mediaLoaded.value = false
videoElement.value.src = ''
})
player.value.on("ended", () => {
mediaLoaded.value = false;
videoElement.value.src = "";
});
}
}
};
const loadVideoSource = async () => {
try {
const { data } = await postApi.play(route.params.id)
const { data } = await postApi.play(route.params.id);
if (videoElement.value) {
mediaLoaded.value = true
videoElement.value.src = data.url
await player.value.restart()
await player.value.play()
mediaLoaded.value = true;
videoElement.value.src = data.url;
await player.value.restart();
await player.value.play();
}
} catch (error) {
console.error('Failed to load video:', error)
alert('视频加载失败: ' + error.response.data)
await player.value.stop()
console.error("Failed to load video:", error);
alert("视频加载失败: " + error.response.data);
await player.value.stop();
}
}
};
const handleBuy = async () => {
if (buying.value) return
buying.value = true
if (buying.value) return;
buying.value = true;
try {
const response = await postApi.buy(article.value.id)
const payData = response.data
const response = await postApi.buy(article.value.id);
const payData = response.data;
// 调用微信支付
window.WeixinJSBridge.invoke('getBrandWCPayRequest', {
...payData
}, function (res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
// 支付成功,刷新文章数据
fetchArticle()
} else {
// 支付失败或取消
console.error('Payment failed:', res.err_msg)
alert('支付失败:' + (res.err_msg === 'get_brand_wcpay_request:cancel' ? '支付已取消' : '支付异常'))
window.WeixinJSBridge.invoke(
"getBrandWCPayRequest",
{
...payData,
},
function (res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功,刷新文章数据
fetchArticle();
} else {
// 支付失败或取消
console.error("Payment failed:", res.err_msg);
alert(
"支付失败:" +
(res.err_msg === "get_brand_wcpay_request:cancel"
? "支付已取消"
: "支付异常")
);
}
}
})
);
} catch (error) {
console.error('Failed to initiate payment:', error)
alert('发起支付失败,请稍后重试')
console.error("Failed to initiate payment:", error);
alert("发起支付失败,请稍后重试");
} finally {
buying.value = false
buying.value = false;
}
}
};
const fetchArticle = async () => {
try {
const { id } = route.params
const { data } = await postApi.show(id)
article.value = data
document.title = article.value.title
const { id } = route.params;
const { data } = await postApi.show(id);
article.value = data;
document.title = article.value.title;
// 调用微信 JS SDK 分享接口
wx.setShareInfo({
title: data.title,
desc: data.content,
title: article.value.title,
desc: article.value.content,
link: window.location.href,
imgUrl: data.head_images[0]
})
imgUrl: data.head_images[0],
});
} catch (error) {
console.error('Failed to fetch article:', error)
alert("加载失败!")
console.error("Failed to fetch article:", error);
alert("加载失败!");
}
}
};
const handleBack = () => {
router.back()
}
router.back();
};
onMounted(async () => {
await fetchArticle()
initializePlayer()
wechatApi
.jsSdk()
.then((resp) => {
wx.initConfig(resp.data).then(() => {
wx.setShareInfo({
title: article.value.title,
desc: article.value.content,
link: window.location.href,
imgUrl: article.value.head_images[0],
});
});
})
.catch((error) => {
console.error("Failed to initialize WeChat SDK:", error);
})
.finally(() => {
wechatApi.jsSdk().then(resp => {
wx.initConfig(resp.data)
}).catch(error => {
console.error('Failed to initialize WeChat SDK:', error)
})
})
});
await fetchArticle();
initializePlayer();
});
onUnmounted(() => {
if (player.value) {
player.value.destroy()
player.value.destroy();
}
})
});
</script>
<template>
@@ -156,7 +184,7 @@ onUnmounted(() => {
<div class="flex items-center justify-between max-w-md mx-auto p-4">
<div class="text-orange-600 text-2xl">
<span class="mr-2 text-xl">&yen;</span>
<span class="font-bold font-mono">
<span class="font-bold font-mono">
{{ (article.price / 100).toFixed(2) }}
</span>
</div>
@@ -184,7 +212,6 @@ onUnmounted(() => {
<div class="animate-spin rounded-full h-8 w-8 border-4 border-gray-200 border-t-blue-600"></div>
</div>
</div>
</div>
</template>
@@ -194,4 +221,4 @@ onUnmounted(() => {
width: 100%;
max-height: 100vh;
}
</style>
</style>

View File

@@ -18,6 +18,7 @@ export default defineConfig({
server: {
port: 3000,
open: true,
allowedHosts: [".jdwan.com", "0.0.0.0", "127.0.0.1", "localhost", "10.1.1.108"],
proxy: {
'/v1': 'http://localhost:8088',
}