feat: support balance pay

This commit is contained in:
Rogee
2025-05-12 19:45:47 +08:00
parent f8678c1197
commit 1fa31a6b71
11 changed files with 186 additions and 41 deletions

View File

@@ -5,12 +5,14 @@ import (
"strconv"
"time"
"quyun/app/jobs"
"quyun/app/models"
"quyun/app/requests"
"quyun/database/conds"
"quyun/database/fields"
"quyun/database/schemas/public/model"
"quyun/providers/ali"
"quyun/providers/job"
"quyun/providers/wepay"
"github.com/go-pay/gopay/wechat/v3"
@@ -28,6 +30,7 @@ type ListQuery struct {
type posts struct {
wepay *wepay.Client
oss *ali.OSSClient
job *job.Job
}
// List posts
@@ -276,6 +279,22 @@ func (ctl *posts) Buy(ctx fiber.Ctx, id int64, user *model.Users) (*wechat.JSAPI
}
payPrice := post.Price * int64(post.Discount) / 100
if user.Balance >= payPrice {
err = models.Users.SetBalance(ctx.Context(), user.ID, user.Balance-payPrice)
if err != nil {
return nil, errors.Wrap(err, "余额支付失败")
}
if err := ctl.job.Add(&jobs.BalancePayNotify{OrderNo: order.OrderNo}); err != nil {
log.Errorf("add job error:%v", err)
return nil, errors.Wrap(err, "Failed to add job")
}
return &wechat.JSAPIPayParams{
AppId: "balance",
}, nil
}
prePayResp, err := ctl.wepay.V3TransactionJsapi(ctx.Context(), func(bm *wepay.BodyMap) {
bm.
Expire(30 * time.Minute).

View File

@@ -0,0 +1,88 @@
package jobs
import (
"context"
"fmt"
"time"
"quyun/app/models"
"quyun/database/fields"
"github.com/pkg/errors"
. "github.com/riverqueue/river"
log "github.com/sirupsen/logrus"
_ "go.ipao.vip/atom"
"go.ipao.vip/atom/contracts"
_ "go.ipao.vip/atom/contracts"
)
var _ contracts.JobArgs = (*BalancePayNotify)(nil)
type BalancePayNotify struct {
OrderNo string `json:"order_no"`
}
func (s BalancePayNotify) InsertOpts() InsertOpts {
return InsertOpts{
Queue: QueueDefault,
Priority: PriorityDefault,
}
}
func (BalancePayNotify) Kind() string { return "balance_pay_notify" }
func (a BalancePayNotify) UniqueID() string { return a.Kind() }
var _ Worker[BalancePayNotify] = (*BalancePayNotifyWorker)(nil)
// @provider(job)
type BalancePayNotifyWorker struct {
WorkerDefaults[BalancePayNotify]
}
func (w *BalancePayNotifyWorker) Work(ctx context.Context, job *Job[BalancePayNotify]) error {
log := log.WithField("job", job.Args.Kind())
log.Infof("[Start] Working on job with strings: %+v", job.Args)
defer log.Infof("[End] Finished %s", job.Args.Kind())
order, err := models.Orders.GetByOrderNo(context.Background(), job.Args.OrderNo)
if err != nil {
log.Errorf("GetByOrderNo error:%v", err)
return err
}
if order.Status != fields.OrderStatusPending {
log.Infof("Order %s is paid, processing...", job.Args.OrderNo)
return JobCancel(fmt.Errorf("Order already paid, currently status: %d", order.Status))
}
order.PaymentMethod = "balance"
order.Status = fields.OrderStatusCompleted
log.Infof("Updated order details: %+v", order)
tx, err := models.Transaction(ctx)
if err != nil {
return errors.Wrap(err, "Transaction error")
}
defer tx.Rollback()
if err := models.Users.BuyPosts(context.Background(), order.UserID, order.PostID, order.Price); err != nil {
log.Errorf("BuyPosts error:%v", err)
return errors.Wrap(err, "BuyPosts error")
}
if err := models.Orders.Update(context.Background(), order); err != nil {
log.Errorf("Update order error:%v", err)
return errors.Wrap(err, "Update order error")
}
if err := tx.Commit(); err != nil {
log.Errorf("Commit error:%v", err)
return errors.Wrap(err, "Commit error")
}
log.Infof("Successfully processed order %s", order.OrderNo)
return nil
}
func (w *BalancePayNotifyWorker) NextRetry(job *Job[BalancePayNotify]) time.Time {
return time.Now().Add(30 * time.Second)
}

View File

@@ -474,3 +474,21 @@ func (m *usersModel) RevokePosts(ctx context.Context, userID, postID int64) erro
}
return nil
}
// SetBalance
func (m *usersModel) SetBalance(ctx context.Context, id int64, balance int64) error {
tbl := table.Users
stmt := tbl.
UPDATE(tbl.Balance).
SET(Int64(balance)).
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 balance: %v", err)
return err
}
return nil
}

View File

@@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE public.users
ADD balance int8 DEFAULT 0 NOT NULL;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE public.users
DROP COLUMN balance;
-- +goose StatementEnd

View File

@@ -19,6 +19,6 @@ type Medias struct {
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Path string `json:"path"`
Hash string `json:"hash"`
Metas fields.Json[fields.MediaMetas] `json:"metas"`
Hash string `json:"hash"`
}

View File

@@ -19,6 +19,7 @@ type Posts struct {
DeletedAt *time.Time `json:"deleted_at"`
Status fields.PostStatus `json:"status"`
Title string `json:"title"`
HeadImages fields.Json[[]int64] `json:"head_images"`
Description string `json:"description"`
Content string `json:"content"`
Price int64 `json:"price"`
@@ -27,5 +28,4 @@ type Posts struct {
Likes int64 `json:"likes"`
Tags fields.Json[[]string] `json:"tags"`
Assets fields.Json[[]fields.MediaAsset] `json:"assets"`
HeadImages fields.Json[[]int64] `json:"head_images"`
}

View File

@@ -23,4 +23,5 @@ type Users struct {
Avatar *string `json:"avatar"`
Metas fields.Json[fields.UserMetas] `json:"metas"`
AuthToken fields.Json[fields.UserAuthToken] `json:"auth_token"`
Balance int64 `json:"balance"`
}

View File

@@ -23,8 +23,8 @@ type mediasTable struct {
MimeType postgres.ColumnString
Size postgres.ColumnInteger
Path postgres.ColumnString
Hash postgres.ColumnString
Metas postgres.ColumnString
Hash postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@@ -71,10 +71,10 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable {
MimeTypeColumn = postgres.StringColumn("mime_type")
SizeColumn = postgres.IntegerColumn("size")
PathColumn = postgres.StringColumn("path")
HashColumn = postgres.StringColumn("hash")
MetasColumn = postgres.StringColumn("metas")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, NameColumn, MimeTypeColumn, SizeColumn, PathColumn, HashColumn, MetasColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, NameColumn, MimeTypeColumn, SizeColumn, PathColumn, HashColumn, MetasColumn}
HashColumn = postgres.StringColumn("hash")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, NameColumn, MimeTypeColumn, SizeColumn, PathColumn, MetasColumn, HashColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, NameColumn, MimeTypeColumn, SizeColumn, PathColumn, MetasColumn, HashColumn}
)
return mediasTable{
@@ -87,8 +87,8 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable {
MimeType: MimeTypeColumn,
Size: SizeColumn,
Path: PathColumn,
Hash: HashColumn,
Metas: MetasColumn,
Hash: HashColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,

View File

@@ -23,6 +23,7 @@ type postsTable struct {
DeletedAt postgres.ColumnTimestamp
Status postgres.ColumnInteger
Title postgres.ColumnString
HeadImages postgres.ColumnString
Description postgres.ColumnString
Content postgres.ColumnString
Price postgres.ColumnInteger
@@ -31,7 +32,6 @@ type postsTable struct {
Likes postgres.ColumnInteger
Tags postgres.ColumnString
Assets postgres.ColumnString
HeadImages postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@@ -78,6 +78,7 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
DeletedAtColumn = postgres.TimestampColumn("deleted_at")
StatusColumn = postgres.IntegerColumn("status")
TitleColumn = postgres.StringColumn("title")
HeadImagesColumn = postgres.StringColumn("head_images")
DescriptionColumn = postgres.StringColumn("description")
ContentColumn = postgres.StringColumn("content")
PriceColumn = postgres.IntegerColumn("price")
@@ -86,9 +87,8 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
LikesColumn = postgres.IntegerColumn("likes")
TagsColumn = postgres.StringColumn("tags")
AssetsColumn = postgres.StringColumn("assets")
HeadImagesColumn = postgres.StringColumn("head_images")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, TitleColumn, DescriptionColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, TagsColumn, AssetsColumn, HeadImagesColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, TitleColumn, DescriptionColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, TagsColumn, AssetsColumn, HeadImagesColumn}
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, TitleColumn, HeadImagesColumn, DescriptionColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, TagsColumn, AssetsColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, TitleColumn, HeadImagesColumn, DescriptionColumn, ContentColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, TagsColumn, AssetsColumn}
)
return postsTable{
@@ -101,6 +101,7 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
DeletedAt: DeletedAtColumn,
Status: StatusColumn,
Title: TitleColumn,
HeadImages: HeadImagesColumn,
Description: DescriptionColumn,
Content: ContentColumn,
Price: PriceColumn,
@@ -109,7 +110,6 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable {
Likes: LikesColumn,
Tags: TagsColumn,
Assets: AssetsColumn,
HeadImages: HeadImagesColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,

View File

@@ -27,6 +27,7 @@ type usersTable struct {
Avatar postgres.ColumnString
Metas postgres.ColumnString
AuthToken postgres.ColumnString
Balance postgres.ColumnInteger
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@@ -77,8 +78,9 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
AvatarColumn = postgres.StringColumn("avatar")
MetasColumn = postgres.StringColumn("metas")
AuthTokenColumn = postgres.StringColumn("auth_token")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, OpenIDColumn, UsernameColumn, AvatarColumn, MetasColumn, AuthTokenColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, OpenIDColumn, UsernameColumn, AvatarColumn, MetasColumn, AuthTokenColumn}
BalanceColumn = postgres.IntegerColumn("balance")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, OpenIDColumn, UsernameColumn, AvatarColumn, MetasColumn, AuthTokenColumn, BalanceColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, StatusColumn, OpenIDColumn, UsernameColumn, AvatarColumn, MetasColumn, AuthTokenColumn, BalanceColumn}
)
return usersTable{
@@ -95,6 +97,7 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
Avatar: AvatarColumn,
Metas: MetasColumn,
AuthToken: AuthTokenColumn,
Balance: BalanceColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,

View File

@@ -92,35 +92,40 @@ const handleBuy = async () => {
buying.value = true;
try {
const response = await postApi.buy(article.value.id);
const payData = response.data;
// 调用微信支付
window.WeixinJSBridge.invoke(
"getBrandWCPayRequest",
{
...payData,
},
async function (res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功,刷新文章数据
fetchArticle();
await updateMediaSource();
} else if (res.err_msg === "get_brand_wcpay_request:cancel") {
// 用户取消支付
console.log("Payment cancelled");
alert("支付已取消");
} else {
// 支付失败或取消
console.error("Payment failed:", res.err_msg);
alert(
"支付失败:" +
(res.err_msg === "get_brand_wcpay_request:cancel"
? "支付已取消"
: "支付异常")
);
if (payData.AppID != "balance") {
// 调用微信支付
window.WeixinJSBridge.invoke(
"getBrandWCPayRequest",
{
...payData,
},
async function (res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功,刷新文章数据
fetchArticle();
await updateMediaSource();
} else if (res.err_msg === "get_brand_wcpay_request:cancel") {
// 用户取消支付
console.log("Payment cancelled");
alert("支付已取消");
} else {
// 支付失败或取消
console.error("Payment failed:", res.err_msg);
alert(
"支付失败:" +
(res.err_msg === "get_brand_wcpay_request:cancel"
? "支付已取消"
: "支付异常")
);
}
}
}
);
);
} else {
alert("余额支付成功");
}
} catch (error) {
console.error("Failed to initiate payment:", error);
alert("发起支付失败,请稍后重试");
@@ -225,7 +230,7 @@ onUnmounted(() => {
</div>
<button @click="handleBuy" :disabled="buying"
class="bg-orange-600 text-white px-8 py-2 rounded hover:bg-orange-500 active:bg-orange-600 transition-colors disabled:opacity-50">
<span v-if="buying">处理中...</span>
<span v-if="buying">购买中...</span>
<span v-else>立即购买</span>
</button>
</div>