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 package http
import ( import (
"time"
"quyun/app/models"
"quyun/database/schemas/public/model" "quyun/database/schemas/public/model"
"quyun/providers/wechat" "quyun/providers/wechat"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/log"
) )
// @provider // @provider
@@ -17,5 +21,31 @@ type wechats struct {
// @Bind url query // @Bind url query
// @Bind user local // @Bind user local
func (ctl *wechats) GetJsSDK(ctx fiber.Ctx, url string, user *model.Users) (*wechat.JsSDK, error) { 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) return ctl.wechat.GetJsSDK(user.AuthToken.Data.StableAccessToken, url)
} }

View File

@@ -3,6 +3,7 @@ package middlewares
import ( import (
"net/url" "net/url"
"strings" "strings"
"time"
"quyun/app/models" "quyun/app/models"
"quyun/pkg/utils" "quyun/pkg/utils"
@@ -70,6 +71,17 @@ func (f *Middlewares) Auth(ctx fiber.Ctx) error {
} }
return ctx.Redirect().To(fullUrl) 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) ctx.Locals("user", user)
return ctx.Next() return ctx.Next()

View File

@@ -5,6 +5,7 @@ import (
"time" "time"
"quyun/app/requests" "quyun/app/requests"
"quyun/database/fields"
"quyun/database/schemas/public/model" "quyun/database/schemas/public/model"
"quyun/database/schemas/public/table" "quyun/database/schemas/public/table"
@@ -385,3 +386,21 @@ func (m *usersModel) UpdateUsername(ctx context.Context, id int64, username stri
} }
return nil 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 { type AccessTokenResponse struct {
ErrorResponse ErrorResponse
AccessToken string `json:"access_token,omitempty"` AccessToken string `json:"access_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"` // seconds 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 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) { func (we *Client) GetAccessToken() (*AccessTokenResponse, error) {
params := map[string]string{ params := map[string]string{
"grant_type": "client_credential", "grant_type": "client_credential",

View File

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

View File

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

View File

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