feat: update
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">¥</span>
|
<span class="mr-2 text-xl">¥</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>
|
||||||
|
|||||||
@@ -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',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user