feat(新功能):新增支付界面;前端新增优惠券;后端新增优惠券逻辑;完善订购详情页面;

developing
张帅 4 weeks ago
parent 23f4938a5b
commit a36eff4595

@ -20,7 +20,7 @@ const routes: Routes = [
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})

@ -23,15 +23,14 @@ import {AuthInterceptor} from "./core/interceptors/auth.interceptor";
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true,
},
],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }
// {
// provide: HTTP_INTERCEPTORS,
// useClass: AuthInterceptor,
// multi: true,
// },

@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
export interface User {
username?: string;
// 其他用户信息字段
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private token: string | null = null;
private currentUser: User | null = null;
constructor() {
// 从本地存储中获取token和用户信息
this.token = localStorage.getItem('token');
const userStr = localStorage.getItem('user');
if (userStr) {
this.currentUser = JSON.parse(userStr);
}
}
getUser(): Observable<User | null> {
return of(this.currentUser);
}
setUser(user: User) {
this.currentUser = user;
localStorage.setItem('user', JSON.stringify(user));
}
setToken(token: string) {
this.token = token;
localStorage.setItem('token', token);
}
getToken(): string | null {
return this.token;
}
logout() {
this.token = null;
this.currentUser = null;
localStorage.removeItem('token');
localStorage.removeItem('user');
}
}

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ArticleBuyPage } from './article-buy.page';
const routes: Routes = [
{
path: '',
component: ArticleBuyPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ArticleBuyPageRoutingModule {}

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ArticleBuyPageRoutingModule } from './article-buy-routing.module';
import { ArticleBuyPage } from './article-buy.page';
import {HomePageModule} from "../home.module";
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ArticleBuyPageRoutingModule,
HomePageModule
],
declarations: [ArticleBuyPage]
})
export class ArticleBuyPageModule {}

@ -0,0 +1,63 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="返回" defaultHref="/home"></ion-back-button>
</ion-buttons>
<ion-title>订购文章</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<!-- 文章信息 -->
<div class="article-info">
<div class="article-preview">
<img [src]="''" class="preview-img" alt="文章封面">
<div class="preview-text">
<p class="title">{{article!.title}}</p>
<p class="desc">{{article!.brief}}</p>
</div>
</div>
</div>
<!-- 价格选项 -->
<div class="price-section">
<div class="price-row">
<span class="permanent-tag">永久有效</span>
<div class="content">
<div class="left">
<span class="type-text">单篇解锁</span>
<span class="discount-tag">{{(price?.discount || 0.3) * 10}}折</span>
</div>
<div class="right">
<span class="original-price">¥{{price?.amount || 1888}}</span>
<span class="current-price">¥{{getDiscountPrice(this.price.amount,this.price.discount)|| 566.4}}</span>
<ion-icon name="checkmark-circle" class="select-icon"></ion-icon>
</div>
</div>
</div>
</div>
<!-- 更多折扣 -->
<div class="more-discount" (click)="showMoreDiscounts()">
<span>更多折扣优惠 >></span>
</div>
<!-- 优惠券 -->
<!-- <div class="coupon-section">-->
<!-- <div class="section-title">优惠券</div>-->
<!-- <div class="coupon-info" (click)="showCoupons()">-->
<!-- <span class="no-coupon">无可用优惠券</span>-->
<!-- <ion-icon name="chevron-forward-outline"></ion-icon>-->
<!-- </div>-->
<!-- </div>-->
<app-coupon-list [coupons]="coupons" [amount]="price.amount" (couponSelected)="couponSelected($event)" ></app-coupon-list>
<!-- 底部固定容器 -->
<div class="bottom-fixed-wrapper">
<!-- 提交按钮 -->
<ion-button expand="block" class="submit-btn" (click)="submitOrder()">
确认支付 ¥{{getDiscountPriceAndCoupon(price!.amount, price!.discount)}}
</ion-button>
<app-lc-fixed-bar></app-lc-fixed-bar>
</div>
</ion-content>

@ -0,0 +1,200 @@
ion-back-button {
--color: #333333;
}
.article-info {
background: white;
padding: 16px;
margin-bottom: 12px;
.article-preview {
display: flex;
gap: 12px;
.preview-img {
width: 120px;
height: 80px;
border-radius: 4px;
object-fit: cover;
}
.preview-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.title {
font-size: 16px;
color: #333;
font-weight: 500;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.desc {
font-size: 14px;
color: #666;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
}
}
.price-section {
background: white;
padding: 16px;
margin-bottom: 12px;
.price-row {
position: relative;
background: #FFF1F0;
padding: 16px;
border-radius: 8px;
.permanent-tag {
position: absolute;
left: 0;
top: 0;
background: #FF4D4F;
font-size: 11px;
color: #FFFFFF;
letter-spacing: 0;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px 0 4px 0;
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
.left {
display: flex;
align-items: center;
gap: 8px;
.type-text {
font-size: 15px;
color: #694518;
letter-spacing: 0;
font-weight: 600;
}
.discount-tag {
font-size: 10px;
color: #E7211A;
letter-spacing: 0;
text-align: center;
font-weight: 500;
background: white;
padding: 1px 6px;
background: #FFE8E8;
border: 1px solid rgba(231,33,26,1);
border-radius: 2px;
}
}
.right {
display: flex;
align-items: center;
gap: 4px;
.original-price {
opacity: 0.75;
font-size: 12px;
color: #8B8D93;
letter-spacing: 0;
text-align: right;
font-weight: 400;
text-decoration: line-through;
}
.current-price {
font-size: 24px;
color: #E7211A;
letter-spacing: -1px;
font-weight: 700;
}
.select-icon {
font-size: 20px;
color: #FF4D4F;
}
}
}
}
}
.more-discount {
padding: 12px 16px;
margin-bottom: 12px;
text-align: center;
span {
font-size: 14px;
color: #FF4D4F;
}
}
.coupon-section {
background: white;
padding: 16px;
.section-title {
font-size: 14px;
color: #333;
margin-bottom: 12px;
}
.coupon-info {
display: flex;
justify-content: space-between;
align-items: center;
.no-coupon {
font-size: 14px;
color: #999;
}
ion-icon {
font-size: 20px;
color: #999;
}
}
}
.bottom-fixed-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
z-index: 100;
.submit-btn {
margin: 16px;
--background: #FF4D4F;
--border-radius: 8px;
height: 44px;
font-size: 16px;
}
}
//
ion-content {
--padding-bottom: 120px;
}

@ -0,0 +1,96 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { NavController, ModalController } from '@ionic/angular';
import { MoreDiscountsComponent } from '../component/more-discounts/more-discounts.component';
import { Price } from '../../shared/model/price';
import {OrderType} from "../../shared/model/order";
import {Article} from "../../shared/model/article";
import {Coupon} from "../../shared/model/coupon";
import {HomeService} from "../home.service";
import {c} from "@angular/core/navigation_types.d-u4EOrrdZ";
@Component({
selector: 'app-article-buy',
templateUrl: './article-buy.page.html',
styleUrls: ['./article-buy.page.scss'],
standalone: false,
})
export class ArticleBuyPage implements OnInit {
article!: Article;
price!: Price;
coupons: Coupon[] = [];
couponAmount:number = 0;
selectCoupon:Coupon|null = null
constructor(
private router: Router,
private navCtrl: NavController,
private homeService:HomeService,
private modalCtrl: ModalController
) {
const navigation = this.router.getCurrentNavigation();
const article = navigation?.extras?.state?.['article'];
const price = navigation?.extras?.state?.['price'];
if (!article || !price) {
this.navCtrl.back();
return;
}
this.article = article;
this.price = price;
console.log(this.price)
}
ngOnInit() {
this.loadCoupons()
}
loadCoupons(){
this.homeService.getCouponList().subscribe(res=>{
res.forEach(res=>{
console.log(this.price)
console.log(res.minAmount)
if(this.getDiscountPrice(this.price.amount,this.price.discount) > res.minAmount){
console.log("可以使用1")
res.canUse = true
}
})
console.log(res)
this.coupons = res;
})
}
getDiscountPrice(originalPrice: number,discount:number): number {
if (!originalPrice) return 0;
return +(originalPrice * discount).toFixed(1) ; // 3折
}
getDiscountPriceAndCoupon(originalPrice: number,discount:number): number {
if (!originalPrice) return 0;
return +(originalPrice * discount).toFixed(1) - this.couponAmount; // 3折
}
couponSelected(coupon:Coupon|null){
this.selectCoupon = coupon;
console.log(coupon)
if(coupon) {
this.couponAmount = coupon.value;
} else {
this.couponAmount = 0;
}
}
async showMoreDiscounts() {
const modal = await this.modalCtrl.create({
component: MoreDiscountsComponent,
cssClass: 'more-discounts-modal'
});
await modal.present();
}
showCoupons() {
// TODO: 实现优惠券选择功能
}
submitOrder() {
// TODO: 实现订单提交功能
this.navCtrl.navigateForward('/home/confirm-order',{
state:{order:{targetId:this.article.eventId,type:OrderType.OrderTypeArticle,amount:this.getDiscountPrice(this.price.amount,this.price.discount)}}
})
}
}

@ -2,9 +2,9 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { ArticleDetailPage } from './article-detail.page';
import { ImagePreviewComponent } from '../component/image-preview/image-preview.component';
import {ArticleDetailPageRoutingModule} from "./article-detail-routing.module";
import {HomePageModule} from "../home.module";
import {ImagePreviewComponent} from "./image-preview/image-preview.component";
@NgModule({
imports: [

@ -3,7 +3,8 @@ import { NavController } from '@ionic/angular';
import { Router } from '@angular/router';
import { ModalController } from '@ionic/angular';
import { Article } from "../../shared/model/article";
import { ImagePreviewComponent } from "../component/image-preview/image-preview.component";
import {ImagePreviewComponent} from "./image-preview/image-preview.component";
@Component({
selector: 'app-article-detail',
@ -77,11 +78,11 @@ export class ArticleDetailPage implements OnInit, AfterViewInit {
this.modalCtrl.create({
component: ImagePreviewComponent,
componentProps: {
imageUrl
imageUrl:imageUrl
},
cssClass: 'image-preview-modal'
}).then(modal => {
modal.present();
});
}
}
}

@ -0,0 +1,20 @@
import {Component, Input, OnInit} from '@angular/core';
import {ModalController} from "@ionic/angular";
@Component({
selector: 'app-image-preview',
templateUrl: './image-preview.component.html',
styleUrls: ['./image-preview.component.scss'],
standalone:false
})
export class ImagePreviewComponent implements OnInit {
constructor(private modalCtrl: ModalController) {}
ngOnInit() {}
@Input() imageUrl: string = '';
dismiss() {
this.modalCtrl.dismiss();
}
}

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ColumnBuyPage } from './column-buy.page';
const routes: Routes = [
{
path: '',
component: ColumnBuyPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ColumnBuyPageRoutingModule {}

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ColumnBuyPageRoutingModule } from './column-buy-routing.module';
import { ColumnBuyPage } from './column-buy.page';
import {HomePageModule} from "../home.module";
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ColumnBuyPageRoutingModule,
HomePageModule
],
declarations: [ColumnBuyPage]
})
export class ColumnBuyPageModule {}

@ -0,0 +1,100 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="" defaultHref="/home"></ion-back-button>
</ion-buttons>
<ion-title>购买专栏</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<!-- 专栏信息 -->
<div class="column-info">
<div class="icon-circle" [ngClass]="getIconImage(column!.title)?.replace('.png', '')" >
<img [src]="'assets/home/' + getIconImage(column!.title)" [alt]="column?.title">
</div>
<div class="column-text">
<h2>{{column?.title}}</h2>
<p>{{column?.brief}}</p>
</div>
</div>
<!-- 价格选项 -->
<div class="price-options">
<div class="price-row">
<!-- 1个月选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '1'" (click)="selectPeriod('1')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.oneMonthPrice || 0, 1)}}/天</div>
<div class="period">1个月</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.oneMonthPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.oneMonthPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
</div>
<!-- 3个月选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '3'" (click)="selectPeriod('3')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.threeMonthsPrice || 0, 3)}}/天</div>
<div class="period">3个月</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.threeMonthsPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.threeMonthsPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
</div>
</div>
<div class="price-row">
<!-- 6个月选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '6'" (click)="selectPeriod('6')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.sixMonthsPrice || 0, 6)}}/天</div>
<div class="period">6个月</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.sixMonthsPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.sixMonthsPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
</div>
<!-- 1年选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '12'" (click)="selectPeriod('12')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.oneYearPrice || 0, 12)}}/天</div>
<div class="period">1年</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.oneYearPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.oneYearPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
</div>
</div>
</div>
<!-- 更多折扣提示 -->
<div class="more-discount" (click)="showMoreDiscounts()">
<span>更多折扣优惠 >></span>
</div>
<!-- 推荐码 -->
<!-- <div class="recommend-code">
<div class="label">推荐码(选填)</div>
<ion-input placeholder="请输入18位字符" [(ngModel)]="recommendCode"></ion-input>
</div> -->
<!-- 优惠券 -->
<!-- <div class="coupon">-->
<!-- <div class="label">优惠券</div>-->
<!-- <div class="no-coupon">无可用优惠券 ></div>-->
<!-- </div>-->
<app-coupon-list [coupons]="coupons" [amount]="columnPrice.amount" (couponSelected)="couponSelected($event)" ></app-coupon-list>
<!-- 底部固定容器 -->
<div class="bottom-fixed-wrapper">
<!-- 提交按钮 -->
<ion-button expand="block" class="submit-btn" (click)="submitOrder()">
确认支付 ¥{{getDiscountPriceAndCoupon(this.getSelectedPrice()|| 0,columnPrice.discount) - couponAmount}}
</ion-button>
<app-lc-fixed-bar></app-lc-fixed-bar>
</div>
</ion-content>

@ -0,0 +1,176 @@
:host {
ion-content {
--background: #f5f5f5;
}
}
.column-info {
background: white;
padding: 16px;
display: flex;
align-items: center;
margin-bottom: 12px;
.icon-circle {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
background: #F5F5F5;
img {
width: 32px;
height: 32px;
object-fit: contain;
}
}
.column-text {
h2 {
margin: 0;
font-weight: bold;
font-size: 18px;
color: #1D1E22;
letter-spacing: 0;
font-weight: 500;
}
p {
margin: 4px 0 0;
font-size: 12px;
color: #8B8D93;
letter-spacing: 0;
font-weight: 400;
}
}
}
.price-options {
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 12px;
.price-row {
display: flex;
justify-content: space-between;
gap: 12px;
.price-card {
flex: 1;
background: white;
border-radius: 8px;
padding: 12px;
position: relative;
border: 1px solid #eee;
&.selected {
border-color: #ff4d4f;
background: #fff1f0;
}
.daily-price {
font-size: 14px;
color: #ff4d4f;
margin-bottom: 8px;
}
.period {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.total-price {
.price {
font-size: 18px;
color: #ff4d4f;
font-weight: bold;
}
.original-price {
font-size: 12px;
color: #999;
text-decoration: line-through;
margin-left: 4px;
}
}
.discount-tag {
position: absolute;
top: 0;
right: 0;
background: #ff4d4f;
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 0 8px 0 8px;
}
}
}
}
.more-discount {
text-align: center;
margin: 12px 0;
span {
color: #ff4d4f;
font-size: 14px;
}
}
.recommend-code, .coupon {
background: white;
padding: 16px;
margin-bottom: 1px;
.label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
ion-input {
--padding-start: 0;
--background: #f5f5f5;
border-radius: 4px;
height: 40px;
}
.no-coupon {
color: #999;
font-size: 14px;
}
}
.bottom-fixed-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
z-index: 100;
}
.submit-btn {
margin: 24px 16px 20px;
--background: #ff4d4f;
--border-radius: 8px;
height: 44px;
font-size: 16px;
}
// Add padding to main content to prevent overlap
ion-content {
--padding-bottom: 120px;
}
ion-back-button {
--color: #333333;
}

@ -0,0 +1,136 @@
import { Component, OnInit } from '@angular/core';
import { Column } from '../../shared/model/column';
import { Router } from '@angular/router';
import { Price } from '../../shared/model/price';
import { NavController, ModalController } from "@ionic/angular";
import { MoreDiscountsComponent } from '../component/more-discounts/more-discounts.component';
import {Coupon} from "../../shared/model/coupon";
import {HomeService} from "../home.service";
@Component({
selector: 'app-column-buy',
templateUrl: './column-buy.page.html',
styleUrls: ['./column-buy.page.scss'],
standalone: false,
})
export class ColumnBuyPage implements OnInit {
column!: Column; // 专栏信息
columnPrice!: Price; // 价格信息
coupons: Coupon[] = [];
couponAmount:number = 0;
selectCoupon:Coupon|null = null
selectedPeriod: '1' | '3' | '6'| '12' = '1'; // 选中的订阅周期
recommendCode: string = ''; // 推荐码
constructor(
private navCtrl: NavController,
private router: Router,
private homeService:HomeService,
private modalCtrl: ModalController
) {
// 获取导航传递的数据
const navigation = this.router.getCurrentNavigation();
const column = navigation?.extras?.state?.['column'];
const columnPrice = navigation?.extras?.state?.['price'];
if (!column || !columnPrice) {
this.navCtrl.back();
return;
}
this.column = column;
this.columnPrice = columnPrice;
}
ngOnInit() {
this.loadCoupons()
}
// 计算每日价格
getDailyPrice(monthPrice: number, months: number): number {
if (!monthPrice) return 0;
return +(monthPrice / (months * 30)).toFixed(1);
}
// 获取折扣价格
getDiscountPrice(originalPrice: number,discount:number): number {
if (!originalPrice) return 0;
return +(originalPrice * discount).toFixed(1) ; // 3折
}
getDiscountPriceAndCoupon(originalPrice: number,discount:number): number {
if (!originalPrice) return 0;
return +( originalPrice* discount).toFixed(1) - this.couponAmount; // 3折
}
// 选择订阅周期
selectPeriod(period: '1' | '3' | '6'|'12') {
this.selectedPeriod = period;
}
// 获取选中周期的价格
getSelectedPrice(): number {
if (!this.columnPrice) return 0;
switch(this.selectedPeriod) {
case '1':
return this.columnPrice.oneMonthPrice;
case '3':
return this.columnPrice.threeMonthsPrice;
case '6':
return this.columnPrice.sixMonthsPrice;
case '12':
return this.columnPrice.oneYearPrice;
default:
return 0;
}
}
loadCoupons(){
this.homeService.getCouponList().subscribe(res=>{
res.forEach(res=>{
if(this.getDiscountPriceAndCoupon(this.getSelectedPrice(),this.columnPrice.discount) > res.minAmount){
console.log("可以使用1")
res.canUse = true
}
})
console.log(res)
this.coupons = res;
})
}
couponSelected(coupon:Coupon|null){
this.selectCoupon = coupon;
if(coupon) {
this.couponAmount = coupon.value;
} else {
this.couponAmount = 0;
}
}
// 提交订单
submitOrder() {
// TODO: 实现订单提交逻辑
}
// 显示更多优惠弹窗
async showMoreDiscounts() {
const modal = await this.modalCtrl.create({
component: MoreDiscountsComponent,
cssClass: 'more-discounts-modal'
});
await modal.present();
}
getIconImage(columnName: string): string {
const iconMap: { [key: string]: string } = {
'盘中宝': 'pzb.png',
'风口研报': 'fkyb.png',
'狙击龙虎榜': 'jjlhb.png',
'电报解读': 'dbjd.png',
'财联社早知道': 'clzzd.png',
'研选': 'yx.png',
'金牌纪要库': 'jpjyk.png',
'九点特供': 'jdtg.png',
'公告全知道': 'ggqzd.png'
};
return iconMap[columnName] || 'default.png';
}
}

@ -39,6 +39,8 @@ export class ArticleContentComponent implements OnInit {
return this.getUnLockData()
case "free":
return this.getFreeData()
default:
return this.getNewData()
}
}

@ -1,4 +1,4 @@
<div class="article-item" [class.locked]="article.lock" (click)="detail()">
<div class="article-item" [class.locked]="!article.unlock" (click)="detail()">
<div class="article-content">
<div class="article-header">
<div class="title-row">
@ -11,8 +11,8 @@
<span class="tag growth-board" *ngIf="article.growthBoard > 0">创业板 <span class="count" style="color:#077BE9">{{article.growthBoard}}只</span></span>
</div>
</div>
<div class="lock-mask" *ngIf="article.lock">
<button class="unlock-button">立即解锁</button>
<div class="lock-mask" *ngIf="!article.unlock">
<button class="unlock-button" (click)="onUnlock()">立即解锁</button>
<button class="unlock-button" [class.noCount]="freeReadCount==0" [disabled]="freeReadCount==0">赠送({{freeReadCount}}次)</button>
</div>
</div>

@ -2,6 +2,8 @@ import {Component, Input, OnInit, Output, EventEmitter} from '@angular/core';
import {Article} from "../../../shared/model/article";
import {NavController} from "@ionic/angular";
import {getUser} from "../../../mine/mine.service";
import {HomeService} from "../../home.service";
import { AlertController } from '@ionic/angular';
@Component({
selector: 'app-article-item',
@ -12,9 +14,11 @@ import {getUser} from "../../../mine/mine.service";
export class ArticleItemComponent implements OnInit {
@Input() article!:Article
@Input() lock:boolean = true;
freeReadCount:number=0;
freeReadCount:number=10;
constructor(private navCtrl: NavController) {
constructor(private navCtrl: NavController,
private alertCtrl:AlertController,
private homeService:HomeService) {
getUser().subscribe((res)=>{
if(res.username != "") {
this.freeReadCount = parseInt(localStorage.getItem("giftCount")!)
@ -25,12 +29,30 @@ export class ArticleItemComponent implements OnInit {
ngOnInit() {}
onUnlock() {
getUser().subscribe((res)=>{
if(res.username == "") {
// 未登录,显示提示框
this.alertCtrl.create({
header: '提示',
message: '请先登录后再进行操作',
buttons: ['确定']
}).then(alert => alert.present());
} else {
this.homeService.getArticlePrice(this.article.eventId).subscribe((res)=>{
console.log(res)
console.log(this.article)
this.navCtrl.navigateForward('/home/article-buy', {
state:{article:this.article,price:res}
});
})
}
})
}
detail(){
console.log("跳转")
if(!this.article.lock && this.article.stocks != "" && this.article.content != "") {
if(this.article.unlock && this.article.stocks != "" && this.article.content != "") {
console.log("跳转")
console.log(this.article)
this.navCtrl.navigateForward('/home/article-detail', {
state:{article:this.article}
});

@ -0,0 +1,18 @@
import { Component, Input, OnInit } from '@angular/core';
import { Coupon } from 'src/app/shared/model/coupon';
@Component({
selector: 'app-coupon-item',
templateUrl: './coupon-item.component.html',
styleUrls: ['./coupon-item.component.scss'],
standalone: false,
})
export class CouponItemComponent implements OnInit {
@Input() coupon!: Coupon;
constructor() { }
ngOnInit() {}
}

@ -0,0 +1,47 @@
<div class="coupon-container">
<div class="coupon-header">
<h2>优惠券</h2>
<span class="count" *ngIf="coupons.length" >{{ couponAmount!= 0 ?'-':'' }}¥ {{couponAmount}}</span>
<div class="no-coupon" *ngIf="!coupons.length">
暂无可用优惠券
</div>
</div>
<ion-radio-group [(ngModel)]="selectedCouponId" (ionChange)="onCouponSelect($event)">
<!-- 不使用优惠券选项 -->
<div class="coupon-item no-use-coupon" [class.active]="selectedCouponId === 'no-use'">
</div>
<div class="coupon-list">
<div class="coupon-item">
<ion-radio value="no-use"
slot="start"
mode="md"></ion-radio>
<div class="name">不使用优惠券</div>
</div>
<div class="coupon-item"
*ngFor="let coupon of coupons"
[class.active]="isCouponSelected(coupon)"
[class.inactive]="!coupon.canUse"
[class.expired]="coupon.status === 'EXPIRED'">
<ion-radio [value]="coupon.id"
[disabled]="!coupon.canUse"
slot="start"
mode="md"></ion-radio>
<div class="coupon-left">
<div class="amount">
<span class="symbol">¥</span>
<span class="value">{{coupon.value}}</span>
</div>
<div class="condition">满{{coupon.minAmount}}元可用</div>
</div>
<div class="coupon-right">
<div class="name">{{coupon.name}}</div>
<div class="date">有效期至 {{coupon.endTime | date:'yyyy.MM.dd'}}</div>
</div>
</div>
</div>
</ion-radio-group>
</div>

@ -0,0 +1,124 @@
.coupon-container {
padding: 16px;
background: #f5f5f5;
margin-bottom: 60px;
.coupon-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 0 16px;
h2 {
font-size: 16px;
font-weight: 500;
color: #333;
margin: 0;
}
.count {
font-size: 13px;
color: #E7211A;
text-align: right;
line-height: 18px;
font-weight: 600;
}
.no-coupon {
font-size: 14px;
color: #999;
}
}
.coupon-list {
.coupon-item {
display: flex;
align-items: center;
background: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
position: relative;
border: 1px solid #e8e8e8;
ion-radio {
margin-right: 12px;
--color: #999;
--color-checked: #ff4d4f;
}
&.active {
background: rgba(255, 77, 79, 0.1);
border-color: #ff4d4f;
.coupon-left {
.amount {
color: #ff4d4f;
}
}
}
&.inactive {
opacity: 0.5;
cursor: not-allowed;
}
&.expired {
opacity: 0.5;
cursor: not-allowed;
background: #f5f5f5;
}
.coupon-left {
width: 120px;
border-right: 1px dashed #e8e8e8;
padding-right: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.amount {
color: #666;
display: flex;
align-items: baseline;
.symbol {
font-size: 16px;
margin-right: 2px;
}
.value {
font-size: 28px;
font-weight: bold;
}
}
.condition {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
.coupon-right {
flex: 1;
padding-left: 16px;
display: flex;
flex-direction: column;
justify-content: center;
.name {
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.date {
font-size: 12px;
color: #999;
}
}
}
}
}

@ -0,0 +1,64 @@
import {Component, OnInit, Output, EventEmitter, AfterViewInit, Input} from '@angular/core';
import { HomeService } from '../../home.service';
import { Coupon } from 'src/app/shared/model/coupon';
import { Observable } from 'rxjs';
@Component({
selector: 'app-coupon-list',
templateUrl: './coupon-list.component.html',
styleUrls: ['./coupon-list.component.scss'],
standalone:false,
})
export class CouponListComponent implements OnInit,AfterViewInit {
@Input() coupons: Coupon[] = [];
@Input() amount: number = 0;
couponAmount:number = 0
selectedCouponId: string | null = null;
@Output() couponSelected = new EventEmitter<Coupon|null>();
constructor(private homeService: HomeService) {
console.log(this.amount)
}
ngAfterViewInit() {
}
ngOnInit() {
}
onCouponSelect(event: any) {
console.log(event)
const selectedCoupon = this.coupons.find(c => c.id === event.detail.value);
if (selectedCoupon && selectedCoupon.canUse) {
console.log(selectedCoupon)
this.couponAmount = selectedCoupon.value;
this.couponSelected.emit(selectedCoupon);
} else {
this.couponSelected.emit(null);
this.couponAmount = 0;
}
}
isCouponSelected(coupon: Coupon): boolean {
return this.selectedCouponId === coupon.id;
}
isCouponAvailable(coupon: Coupon): boolean {
// 检查优惠券是否可用
if (coupon.status !== 'ACTIVE') {
return false;
}
// 检查订单金额是否满足优惠券使用条件
if (this.amount < coupon.minAmount) {
return false;
}
// 检查优惠券是否在有效期内
const now = new Date();
if (now < new Date(coupon.startTime) || now > new Date(coupon.endTime)) {
return false;
}
return true;
}
}

@ -1,18 +0,0 @@
import { Component, Input } from '@angular/core';
import { ModalController } from '@ionic/angular';
@Component({
selector: 'app-image-preview',
templateUrl:'./image-preview.component.html',
styleUrls: ['./image-preview.component.scss'],
standalone: false
})
export class ImagePreviewComponent {
@Input() imageUrl: string = '';
constructor(private modalCtrl: ModalController) {}
dismiss() {
this.modalCtrl.dismiss();
}
}

@ -0,0 +1,16 @@
<div class="more-discounts">
<div class="header">
<h2>WELCOME TO CAIJIANZUA</h2>
<ion-icon name="close-outline" class="close-btn" (click)="dismiss()"></ion-icon>
</div>
<div class="content">
<div class="title-group">
<div class="main-title">请联系客服</div>
<div class="sub-title">领取<span>现金优惠券</span></div>
</div>
<div class="qr-code">
<img src="assets/ewm.png" alt="客服二维码">
</div>
</div>
</div>

@ -0,0 +1,147 @@
.more-discounts {
background: white;
border-radius: 12px;
padding: 24px;
width: 280px;
margin: 0 auto;
position: relative;
.header {
text-align: center;
margin-bottom: 20px;
position: relative;
h2 {
margin: 0;
font-size: 14px;
font-weight: normal;
color: #333;
text-transform: uppercase;
}
.close-btn {
position: absolute;
right: -12px;
top: -12px;
font-size: 24px;
color: #999;
cursor: pointer;
}
}
.content {
text-align: center;
.title-group {
margin-bottom: 24px;
display: flex;
flex-direction: column;
align-items: flex-start;
.main-title {
font-size: 28px;
color: #1D1E22;
letter-spacing: 0;
line-height: 40px;
font-weight: 600;
}
.sub-title {
font-size: 32px;
color: #1D1E22;
letter-spacing: 0;
line-height: 45px;
font-weight: 600;
span {
color: #E7211A;
}
}
}
.qr-code {
width: 160px;
height: 160px;
margin: 0 auto;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
.discount-list {
.discount-item {
margin-bottom: 24px;
.label {
font-size: 14px;
color: #333;
margin-bottom: 12px;
}
ion-input {
--padding-start: 12px;
--background: #f5f5f5;
border-radius: 8px;
height: 44px;
}
.coupon-list {
.coupon-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #f5f5f5;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
.coupon-info {
.amount {
font-size: 18px;
color: #ff4d4f;
font-weight: 500;
margin-bottom: 4px;
}
.condition {
font-size: 12px;
color: #999;
}
}
.coupon-status {
font-size: 14px;
color: #666;
&.selected {
color: #ff4d4f;
}
}
}
}
.no-coupon {
color: #999;
font-size: 14px;
text-align: center;
padding: 20px 0;
}
}
}
.footer {
margin-top: 32px;
ion-button {
--background: #ff4d4f;
--border-radius: 8px;
height: 44px;
font-size: 16px;
margin: 0;
}
}
}

@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import { ModalController } from '@ionic/angular';
@Component({
selector: 'app-more-discounts',
templateUrl: './more-discounts.component.html',
styleUrls: ['./more-discounts.component.scss'],
standalone: false,
})
export class MoreDiscountsComponent {
constructor(private modalCtrl: ModalController) {}
dismiss() {
this.modalCtrl.dismiss();
}
}

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ConfirmOrderPage } from './confirm-order.page';
const routes: Routes = [
{
path: '',
component: ConfirmOrderPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ConfimOrderPageRoutingModule {}

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ConfimOrderPageRoutingModule } from './confirm-order-routing.module';
import { ConfirmOrderPage } from './confirm-order.page';
import {HomePageModule} from "../home.module";
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ConfimOrderPageRoutingModule,
HomePageModule
],
declarations: [ConfirmOrderPage]
})
export class ConfirmOrderPageModule {}

@ -0,0 +1,35 @@
<ion-header class="ion-no-border">
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="back()" class="cancel-btn">
取消
</ion-button>
</ion-buttons>
<ion-title>支付订单</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<!-- 支付金额 -->
<div class="amount-section">
<div class="amount">¥{{order.amount}}</div>
<div class="desc">{{order.description}}</div>
</div>
<!-- 支付方式 -->
<div class="payment-method">
<div class="method-item">
<ion-icon name="logo-wechat"></ion-icon>
<span>微信支付</span>
<ion-icon name="checkmark" class="check"></ion-icon>
</div>
</div>
</ion-content>
<ion-footer>
<div class="footer-content">
<ion-button expand="block" (click)="submitOrder()" [disabled]="loading">
{{loading ? '支付中...' : '立即支付'}}
</ion-button>
</div>
</ion-footer>

@ -0,0 +1,147 @@
ion-content {
--background: #f8f8f8;
}
.product-info {
background: #fff;
padding: 20px;
border-radius: 12px;
margin-bottom: 12px;
.product-name {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.product-type {
font-size: 14px;
color: #666;
}
}
.price-info {
background: #fff;
padding: 20px;
border-radius: 12px;
margin-bottom: 12px;
.price-item {
display: flex;
justify-content: space-between;
align-items: center;
.label {
font-size: 15px;
color: #666;
}
.value {
font-size: 30px;
color: #f00;
font-weight: 600;
}
}
}
.amount-section {
padding: 32px 0;
text-align: center;
background: #fff;
margin: 12px 0;
border-radius: 12px;
.amount {
font-size: 32px;
font-weight: 600;
color: #000;
margin-bottom: 8px;
}
.desc {
font-size: 14px;
color: #666;
}
}
.payment-method {
background: #fff;
border-radius: 12px;
padding: 0 16px;
.section-title {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 16px;
}
.method-item {
display: flex;
align-items: center;
padding: 16px 0;
position: relative;
ion-icon[name="logo-wechat"] {
font-size: 24px;
margin-right: 12px;
color: #07C160;
}
span {
flex: 1;
font-size: 16px;
color: #333;
}
.check {
font-size: 20px;
color: #07C160;
}
}
}
ion-footer {
.footer-content {
padding: 16px;
background: #fff;
ion-button {
--background: #07C160;
--background-activated: #06ae56;
--border-radius: 24px;
height: 48px;
font-size: 16px;
margin: 0;
&.button-disabled {
--background: #9ed4bb;
}
}
}
}
ion-header {
ion-toolbar {
--background: transparent;
--border-width: 0;
--padding-top: 12px;
--padding-bottom: 12px;
ion-title {
font-size: 17px;
font-weight: 500;
color: #000;
}
.cancel-btn {
font-size: 15px;
font-weight: normal;
color: #999;
--padding-start: 16px;
--padding-end: 16px;
height: 32px;
}
}
}

@ -0,0 +1,121 @@
import { Component, OnInit } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';
import { NavController } from '@ionic/angular';
import {Order, OrderType} from 'src/app/shared/model/order';
import { PaymentService } from 'src/app/services/payment.service';
import { finalize } from 'rxjs/operators';
import {HomeService} from "../home.service";
@Component({
selector: 'app-confirm-order',
templateUrl: './confirm-order.page.html',
styleUrls: ['./confirm-order.page.scss'],
standalone: false,
})
export class ConfirmOrderPage implements OnInit {
order!: Order;
orderType = OrderType;
loading = false;
selectedPaymentMethod = 'wechat';
constructor(
private router: Router,
private navCtrl: NavController,
private homeService: HomeService
) {
const navigation = this.router.getCurrentNavigation();
const order = navigation?.extras?.state?.['order'];
console.log(order)
if (!order) {
this.navCtrl.back();
return;
}
this.order = order;
}
ngOnInit() {
}
selectPaymentMethod(method: string) {
this.selectedPaymentMethod = method;
}
back(){
this.navCtrl.back()
}
submitOrder() {
if (this.loading) return;
this.loading = true;
if (this.selectedPaymentMethod === 'wechat') {
// 创建支付订单并唤起微信支付
this.homeService.createOrder(this.order).subscribe({
next: (payment) => {
// 唤起微信支付
// this.paymentService.callWechatPay(payment.orderNo).subscribe({
// next: () => {
// // 跳转到支付结果页面
// const navigationExtras: NavigationExtras = {
// state: {
// payment
// }
// };
//
// this.router.navigate(['/payment-result'], navigationExtras);
// },
// error: (error) => {
// console.error('微信支付失败:', error);
// // TODO: 显示错误提示
// }
// });
},
error: (error) => {
console.error('创建支付订单失败:', error);
// TODO: 显示错误提示
},
complete: () => {
this.loading = false;
}
});
} else {
// 支付宝支付逻辑
// this.paymentService.createPayment({
// orderNo: this.order.orderNo,
// amount: this.order.amount,
// paymentType: 'alipay'
// }).subscribe({
// next: (payment) => {
// // 跳转到支付结果页面
// const navigationExtras: NavigationExtras = {
// state: {
// payment
// }
// };
//
// this.router.navigate(['/payment-result'], navigationExtras);
// },
// error: (error) => {
// console.error('创建支付订单失败:', error);
// // TODO: 显示错误提示
// },
// complete: () => {
// this.loading = false;
// }
// });
}
}
// 获取订单类型文本
getOrderTypeText(): string {
return this.order.type === OrderType.OrderTypeArticle ? '文章' : '专栏';
}
// 获取有效期文本
getDurationText(): string {
if (this.order.type === OrderType.OrderTypeArticle) {
return '永久有效';
}
return `${this.order.duration}个月`;
}
}

@ -14,7 +14,20 @@ const routes: Routes = [
{
path: 'article-detail',
loadChildren: () => import('./article-detail/article-detail.module').then( m => m.ArticleDetailModule)
},
{
path: 'column-buy',
loadChildren: () => import('./column-buy/column-buy.module').then( m => m.ColumnBuyPageModule)
},
{
path: 'article-buy',
loadChildren: () => import('./article-buy/article-buy.module').then( m => m.ArticleBuyPageModule)
},
{
path: 'confirm-order',
loadChildren: () => import('./confirm-order/confirm-order.module').then(m => m.ConfirmOrderPageModule)
}
];
@NgModule({

@ -8,6 +8,8 @@ import { HomeIconsComponent } from './component/home-icons/home-icons.component'
import { ArticleItemComponent } from "./component/article-item/article-item.component";
import { ArticleContentComponent } from "./component/article-content/article-content.component";
import {LcFixedBarComponent} from "./component/lc-fixed-bar/lc-fixed-bar.component";
import {MoreDiscountsComponent} from "./component/more-discounts/more-discounts.component";
import {CouponListComponent} from "./component/coupon-list/coupon-list.component";
@NgModule({
imports: [
@ -22,11 +24,14 @@ import {LcFixedBarComponent} from "./component/lc-fixed-bar/lc-fixed-bar.compone
HomeIconsComponent,
ArticleItemComponent,
ArticleContentComponent,
LcFixedBarComponent
LcFixedBarComponent,
MoreDiscountsComponent,
CouponListComponent,
],
exports: [
LcFixedBarComponent,
ArticleContentComponent
ArticleContentComponent,
CouponListComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

@ -4,7 +4,10 @@ import {map, Observable} from "rxjs";
import {Article} from "../shared/model/article";
import {extractData, Page} from "../shared/model/page";
import {HttpUtils} from "../shared/until/http.utils";
import {Price} from "../shared/model/price";
import {Column} from "../shared/model/column";
import {Order} from '../shared/model/order';
import { Coupon } from '../shared/model/coupon';
@Injectable({
providedIn: 'root'
})
@ -32,4 +35,21 @@ export class HomeService {
getArticleDetail(uid:number):Observable<Article>{
return this.http.get<Article>(`/api/article/detail/${uid}`)
}
getArticlePrice(targetId:number):Observable<Price>{
return this.http.get<Price>(`/api/price/article/${targetId}`)
}
getColumnPrice(targetId:number):Observable<Price>{
return this.http.get<Price>(`/api/price/column/${targetId}`)
}
getColumnByName(title:string):Observable<Column>{
return this.http.get<Column>(`/api/column/get`,{params:{title}})
}
createOrder(order:Order):Observable<any> {
return this.http.post<any>('/api/order/create',order)
}
getCouponList():Observable<Array<Coupon>> {
console.log("开始请求")
return this.http.get<Array<Coupon>>('/api/coupon/get',{})
}
}

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ColumnDescribePage } from './column-describe.page';
const routes: Routes = [
{
path: '',
component: ColumnDescribePage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ColumnDescribePageRoutingModule {}

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ColumnDescribePageRoutingModule } from './column-describe-routing.module';
import { ColumnDescribePage } from './column-describe.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ColumnDescribePageRoutingModule
],
declarations: [ColumnDescribePage]
})
export class ColumnDescribePageModule {}

@ -0,0 +1,12 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="" defaultHref="/home"></ion-back-button>
</ion-buttons>
<ion-title>{{ column.title }}简介</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<img [src]="'assets/class_des/' + getIconImage(column!.title)" [alt]="column?.title">
</ion-content>

@ -0,0 +1,41 @@
import { Component, OnInit } from '@angular/core';
import {Column} from "../../../shared/model/column";
import {NavController} from "@ionic/angular";
import {Router} from "@angular/router";
@Component({
selector: 'app-column-describe',
templateUrl: './column-describe.page.html',
styleUrls: ['./column-describe.page.scss'],
standalone:false,
})
export class ColumnDescribePage implements OnInit {
column!:Column;
constructor( private navCtrl: NavController,
private router: Router,) {
const navigation = this.router.getCurrentNavigation();
const column = navigation?.extras?.state?.['column'];
if (column) {
this.column = column;
} else {
this.navCtrl.back()
}
}
ngOnInit() {
}
getIconImage(columnName: string): string {
const iconMap: { [key: string]: string } = {
'盘中宝': 'pzb.png',
'风口研报': 'fkyb.png',
'狙击龙虎榜': 'jjlhb.png',
'电报解读': 'dbjd.png',
'财联社早知道': 'clzzd.png',
'研选': 'yx.png',
'金牌纪要库': 'jpjyk.png',
'九点特供': 'jdtg.png',
'公告全知道': 'ggqzd.png'
};
return iconMap[columnName] || 'default.png';
}
}

@ -1,17 +1,21 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SpecialColumnPage } from './special-column.page';
const routes: Routes = [
{
path: '',
component: SpecialColumnPage
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SpecialColumnPage } from './special-column.page';
const routes: Routes = [
{
path: '',
component: SpecialColumnPage
}, {
path: 'column-describe',
loadChildren: () => import('./column-describe/column-describe.module').then( m => m.ColumnDescribePageModule)
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SpecialColumnPageRoutingModule {}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SpecialColumnPageRoutingModule {}

@ -1,5 +1,8 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="" defaultHref="/home"></ion-back-button>
</ion-buttons>
<ion-title>{{name}}</ion-title>
</ion-toolbar>
</ion-header>
@ -16,7 +19,7 @@
<div class="column-info">
<div class="title-row">
<h1>{{name}}</h1>
<div class="tag">简介</div>
<div class="tag" (click)="describe()">简介 ></div>
</div>
<div class="stats-row">
<div class="stat-item">
@ -37,3 +40,12 @@
<app-article-content></app-article-content>
<app-lc-fixed-bar></app-lc-fixed-bar>
</ion-content>
<!-- 底部固定按钮 -->
<div class="fixed-bottom" *ngIf="!column?.unlock">
<div class="price">
<p class="p1">盘中宝</p>
<p class="p2">重磅信息挖掘</p>
</div>
<button class="buy-btn" (click)="onBuyClick()">立即订购</button>
</div>

@ -74,16 +74,19 @@
h1 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #2F3033;
color: #1D1E22;
letter-spacing: 0;
font-weight: 900;
}
.tag {
background: #F5F5F5;
color: #666;
font-size: 12px;
height: 20px;
background: #1D1E22;
font-size: 11px;
color: rgba(255,255,255,0.80);
font-weight: 400;
padding: 2px 8px;
border-radius: 4px;
border-radius: 2px;
}
}
@ -96,16 +99,13 @@
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #78787A;
font-weight: 400;
.number {
font-size: 14px;
color: #2F3033;
font-weight: 500;
}
.label {
font-size: 14px;
color: #666;
}
}
}
@ -131,3 +131,65 @@
}
}
}
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(90deg, #474D5D 0%, #333742 75%);
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
padding-left:16px;
.price {
text-align: center;
display: flex;
flex-direction: column;
align-items: flex-start;
.p1, .p2 {
margin: 0;
line-height: 1.2;
}
.p1 {
font-size: 16px;
font-weight: 500;
color: #fff;
}
.p2 {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin-top: 2px;
}
}
.buy-btn {
background: linear-gradient(180deg, #F54545 0%, #DC2D2D 100%);
color: white;
border: none;
padding: 8px 24px;
font-size: 16px;
color: #FFFFFF;
text-align: center;
font-weight: 400;
cursor: pointer;
height:60px;
width:40%;
&:active {
background: darken(#e74c3c, 10%);
}
}
}
//
ion-content {
--padding-bottom: 50px;
}
ion-back-button {
--color: #333333;
}

@ -1,26 +1,75 @@
import { Component, OnInit } from '@angular/core';
import {ModalController, NavController} from "@ionic/angular";
import {Router} from "@angular/router";
import { ModalController, NavController, AlertController } from "@ionic/angular";
import { Router } from "@angular/router";
import { HomeService } from '../home.service';
import { Column } from '../../shared/model/column';
import { getUser } from "../../mine/mine.service";
@Component({
selector: 'app-special-column',
templateUrl: './special-column.page.html',
styleUrls: ['./special-column.page.scss'],
standalone:false,
standalone: false,
})
export class SpecialColumnPage implements OnInit {
name:string = ""
constructor(private navCtrl: NavController,
private router: Router,
) {
name: string = ""
column!: Column;
constructor(
private navCtrl: NavController,
private router: Router,
private homeService: HomeService,
private modalCtrl: ModalController,
private alertCtrl: AlertController
) {
const navigation = this.router.getCurrentNavigation();
const name = navigation?.extras?.state?.['name'];
if (name) {
this.name = name;
this.getColumnData();
}
}
ngOnInit() {
}
getColumnData() {
this.homeService.getColumnByName(this.name).subscribe(data => {
console.log(data)
this.column = data;
console.log(this.column)
})
}
onBuyClick() {
// 检查用户是否已登录
getUser().subscribe(user => {
if (!user?.username) {
// 未登录,显示提示框
this.alertCtrl.create({
header: '提示',
message: '请先登录后再进行操作',
buttons: ['确定']
}).then(alert => alert.present());
return;
}
// 已登录,获取价格并跳转到购买页面
this.getColumnPrice();
});
}
getColumnPrice() {
this.homeService.getColumnPrice(this.column.id).subscribe((res)=>{
this.navCtrl.navigateForward('/home/column-buy', {
state: { column: this.column, price: res }
});
})
}
describe(){
this.navCtrl.navigateForward('/home/special-column/column-describe', {
state: { column: this.column}
});
}
}

@ -7,7 +7,7 @@
<!-- 登录方式切换 -->
<div class="login-type-switch">
<span [class.active]="loginType === 'sms'" (click)="loginType = 'sms'">验证码登录</span>
<span [class.active]="loginType === 'password'" (click)="loginType = 'password'">密码登录</span>
<!-- <span [class.active]="loginType === 'password'" (click)="loginType = 'password'">密码登录</span>-->
</div>
<!-- 登录表单 -->

@ -10,7 +10,7 @@ import {
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import {ModalController, ToastController} from '@ionic/angular';
import {ModalController, NavController, ToastController} from '@ionic/angular';
import { MineService } from '../mine.service';
import {
SlidePoint,
@ -37,6 +37,7 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit {
private fb: FormBuilder,
private mineService: MineService,
private router: Router,
private navCtrl: NavController,
private cdr:ChangeDetectorRef,
private toastCtrl: ToastController,
private modalCtrl: ModalController,
@ -165,7 +166,7 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit {
console.log('登录成功:', response);
this.showToast('登录成功');
sessionStorage.setItem("token",response)
this.router.navigate(['/mine']);
this.navCtrl.back()
},
error: (error) => {
console.error('登录失败:', error);

@ -2,7 +2,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ToastController } from '@ionic/angular';
import {getUser, MineService} from './mine.service';
import {delay} from "rxjs";
import { Router } from '@angular/router';
import {UserInfo} from "../shared/model/user";

@ -20,7 +20,7 @@ export interface LoginResponse {
providedIn: 'root'
})
export class MineService {
private apiUrl = environment.apiUrl;
constructor(private http: HttpClient, private modalController: ModalController) {}

@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { Observable, from } from 'rxjs';
import { map, tap, mergeMap } from 'rxjs/operators';
import {Payment, WechatPayParams} from "../shared/model/order";
declare const WeixinJSBridge: any;
@Injectable({
providedIn: 'root'
})
export class PaymentService {
private apiUrl = `${environment.apiUrl}/payments`;
constructor(private http: HttpClient) {}
createPayment(request: PaymentRequest): Observable<Payment> {
return this.http.post<Payment>(this.apiUrl, request);
}
getPayment(orderNo: string): Observable<Payment> {
return this.http.get<Payment>(`${this.apiUrl}/${orderNo}`);
}
getPaymentStatus(orderNo: string): Observable<{ status: string }> {
return this.http.get<{ status: string }>(`${this.apiUrl}/${orderNo}/status`);
}
getWechatPayParams(orderNo: string): Observable<WechatPayParams> {
return this.http.get<WechatPayParams>(`${this.apiUrl}/${orderNo}/wechat-pay`);
}
// 唤起微信支付
callWechatPay(orderNo: string): Observable<void> {
return this.getWechatPayParams(orderNo).pipe(
map(params => {
if (typeof WeixinJSBridge === 'undefined') {
throw new Error('请在微信浏览器中打开');
}
return new Promise<void>((resolve, reject) => {
WeixinJSBridge.invoke('getBrandWCPayRequest', params, (res: any) => {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
console.log('支付成功');
resolve();
} else {
console.error('支付失败:', res.err_msg);
reject(new Error(res.err_msg));
}
});
});
}),
mergeMap(promise => from(promise)),
tap({
error: (error) => console.error('微信支付失败:', error)
})
);
}
}

@ -1,5 +1,5 @@
export interface Article{
eventId:string;
eventId:number;
title:string;
class:string;
releaseDate:string;
@ -9,6 +9,6 @@ export interface Article{
content:string;
mainBoard:number;
growthBoard:number;
lock:boolean;
unlock:boolean;
}

@ -0,0 +1,10 @@
export interface Column {
id:number;
title:string;
brief:string;
cover:string;
articleNum:number;
followNum:number;
purchaseNum:number;
unlock:boolean;
}

@ -0,0 +1,25 @@
export interface Coupon {
id: string;
name: string;
type: CouponType;
value: number;
minAmount: number;
startTime: Date;
endTime: Date;
status: CouponStatus;
adminId: string;
userId: string;
canUse:boolean;
}
export enum CouponType {
FULL_REDUCTION = 'FULL_REDUCTION',
DISCOUNT = 'DISCOUNT',
}
export enum CouponStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
EXPIRED = 'EXPIRED',
}

@ -0,0 +1,42 @@
export enum OrderType {
OrderTypeArticle = 1,
OrderTypeColumn = 2
}
export interface Order {
id: number;
orderNo: string;
targetId: number;
type: number;
amount: number;
duration: number;
status: number;
description: string;
createdAt: string;
updatedAt: string;
}
export interface PaymentRequest {
orderNo: string;
amount: number;
paymentType: 'wechat' | 'alipay';
}
export interface Payment {
id: number;
orderNo: string;
amount: number;
paymentType: string;
status: string;
createdAt: string;
updatedAt: string;
}
export interface WechatPayParams {
appId: string;
timeStamp: string;
nonceStr: string;
package: string;
signType: string;
paySign: string;
}

@ -0,0 +1,11 @@
export interface Price {
id: number;
targetId: number;
type: string;
amount: number;
oneMonthPrice: number;
threeMonthsPrice: number;
sixMonthsPrice: number;
oneYearPrice: number;
discount: number;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@ -84,3 +84,15 @@ go-captcha {
}
}
}
//
.more-discounts-modal {
--backdrop-opacity: 0.6;
&::part(content) {
--width: fit-content;
--height: fit-content;
--background: transparent;
--box-shadow: none;
}
}

@ -1,13 +1,14 @@
module cls
go 1.18
go 1.23.0
toolchain go1.23.7
require (
github.com/ClickHouse/clickhouse-go/v2 v2.6.0
github.com/appleboy/gin-jwt/v2 v2.9.1
github.com/cenkalti/backoff/v4 v4.2.0
github.com/gin-gonic/gin v1.8.2
github.com/go-redis/redis_rate/v9 v9.1.2
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-jwt/jwt/v4 v4.4.3
github.com/json-iterator/go v1.1.12
@ -38,7 +39,6 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/go-redis/redis/v8 v8.11.4 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
@ -51,6 +51,8 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.16.0 // indirect
github.com/paulmach/orb v0.8.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
@ -71,7 +73,7 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/dig v1.16.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/image v0.25.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.23.0 // indirect

@ -71,19 +71,21 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -131,6 +133,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@ -163,10 +166,6 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-redis/redis_rate/v9 v9.1.2 h1:H0l5VzoAtOE6ydd38j8MCq3ABlGLnvvbA1xDSVVCHgQ=
github.com/go-redis/redis_rate/v9 v9.1.2/go.mod h1:oam2de2apSgRG8aJzwJddXbNu91Iyz1m8IKJE2vpvlQ=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@ -233,7 +232,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -618,6 +616,7 @@ go.uber.org/dig v1.16.0/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
go.uber.org/fx v1.19.1 h1:JwYIYAQzXBuBBwSZ1/tn/95pnQO/Sp3yE8lWj9eSAzI=
go.uber.org/fx v1.19.1/go.mod h1:bGK+AEy7XUwTBkqCsK/vDyFF0JJOA6X5KWpNC0e6qTA=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -668,9 +667,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -775,6 +773,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

@ -7,36 +7,39 @@ import (
domainUser "cls/internal/domain/user"
"cls/pkg/logger"
"cls/pkg/util/page"
"errors"
"fmt"
"strings"
"time"
"xorm.io/builder"
)
type ArticleService struct {
repo article.ArticleRepository
userRepo domainUser.UserRepository
purchaseRepo purchase.Repository
FreeRepo free_trial.FreeTrialRepository
resp *ArticleResp
log logger.Logger
repo article.ArticleRepository
userRepo domainUser.UserRepository
purchaseRepo purchase.Repository
userAggregateRepo domainUser.UserAggregateRepository
FreeRepo free_trial.FreeTrialRepository
resp *ArticleResp
log logger.Logger
}
func NewArticleService(repo article.ArticleRepository,
userRepo domainUser.UserRepository,
purchaseRepo purchase.Repository,
userAggregateRepo domainUser.UserAggregateRepository,
FreeRepo free_trial.FreeTrialRepository,
resp *ArticleResp,
log logger.New) *ArticleService {
return &ArticleService{repo,
userRepo,
purchaseRepo,
userAggregateRepo,
FreeRepo,
resp,
log("cls:service:article")}
}
var class_type = map[int64]string{
var class_type = map[uint64]string{
20014: "狙击龙虎榜",
20015: "盘中宝",
20021: "风口研报",
@ -47,7 +50,7 @@ var class_type = map[int64]string{
20087: "金牌纪要库",
}
var class_type_reverse = map[string]int64{
var class_type_reverse = map[string]uint64{
"狙击龙虎榜": 20014,
"盘中宝": 20015,
"风口研报": 20021,
@ -75,19 +78,31 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s
a.log.Error(err)
return err
}
user, err := a.userRepo.FindByPhone(ePhone)
if err != nil {
a.log.Error(err)
}
articleIds := make([]uint64, 0, len(articles))
for _, v := range articles {
articleIds = append(articleIds, v.Id)
}
purchaseId := make(map[uint64]struct{})
if user != nil {
purchaseData, err := a.purchaseRepo.FindArticleById(articleIds...)
if ePhone != "" {
user, err := a.userRepo.FindByPhone(ePhone)
if err != nil {
a.log.Error(err)
}
columnData, err := a.purchaseRepo.FindColumnsByUserId(user.Id)
columnMap := make(map[uint64]struct{})
if len(columnData) != 0 {
for _, v := range columnData {
columnMap[v.ContentId] = struct{}{}
}
}
articleIds := make([]uint64, 0, len(articles))
for _, v := range articles {
_, has := columnMap[v.Type]
if has {
purchaseId[v.Id] = struct{}{}
} else {
articleIds = append(articleIds, v.Id)
}
}
articleIds = nil
purchaseData, err := a.purchaseRepo.FindArticleById(user.Id, articleIds...)
if err != nil {
a.log.Error(err.Error())
}
@ -96,8 +111,10 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s
purchaseId[v.ContentId] = struct{}{}
}
}
articleIds = nil
}
articleIds = nil
result := make([]*ArticleDto, 0, len(articles))
for _, v := range articles {
t := time.Unix(v.Ctime, 0) // 秒数和纳秒数0 表示没有纳秒部分
@ -115,6 +132,7 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s
Unlock: lock,
})
}
purchaseId = nil
articles = nil
page.Content = &result
return nil
@ -261,42 +279,32 @@ func (a *ArticleService) Detail(userPhone string, id uint64) (*ArticleDto, error
}
func (a *ArticleService) UnLockArticle(ePhone string, aid uint64) (*ArticleDto, error) {
user, err := a.userRepo.FindByPhone(ePhone)
// 1. 获取用户聚合根
userAggregate, err := a.userAggregateRepo.GetUserAggregate(ePhone)
if err != nil {
a.log.Error(err.Error())
return nil, err
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
if user.GiftCount-1 < 0 {
a.log.Errorf("用户【%d】赠送次数不够", user.Id)
return nil, errors.New("赠送次数用尽")
}
ar, err := a.repo.GetArticleById(aid)
// 2. 获取文章
article, err := a.repo.GetArticleById(aid)
if err != nil {
a.log.Error(err.Error())
return nil, err
return nil, fmt.Errorf("获取文章信息失败: %w", err)
}
err = a.userRepo.UpdateUserGiftCount(user.Id, user.GiftCount-1)
if err != nil {
a.log.Error(err.Error())
return nil, err
// 3. 执行领域逻辑
if err := userAggregate.UnlockArticle(aid); err != nil {
return nil, fmt.Errorf("解锁文章失败: %w", err)
}
pr := &purchase.Purchase{
UserId: user.Id,
ContentId: aid,
ContentType: purchase.ContentTypeArticle,
ContentSource: purchase.ContentSourceGift,
}
err = a.purchaseRepo.Save(pr)
if err != nil {
a.log.Error(err.Error())
return nil, err
// 4. 保存聚合根状态
if err := a.userAggregateRepo.SaveUserAggregate(userAggregate); err != nil {
return nil, fmt.Errorf("保存用户状态失败: %w", err)
}
// 5. 返回结果
return &ArticleDto{
Stocks: ar.Stocks,
Content: ar.Content,
Stocks: article.Stocks,
Content: article.Content,
Unlock: true,
}, nil
}

@ -0,0 +1,42 @@
package column
import "cls/internal/domain/column"
// ToDto 将领域对象转换为DTO
func ToDto(col *column.Column) *ColumnDto {
if col == nil {
return nil
}
return &ColumnDto{
ID: col.ID,
Title: col.Title,
Brief: col.Brief,
Cover: col.Cover,
AuthorID: col.AuthorID,
Status: col.Status,
ArticleNum: col.ArticleNum,
CreatedAt: col.CreatedAt,
}
}
// ToDtoList 将领域对象列表转换为DTO列表
func ToDtoList(cols []*column.Column) []*ColumnDto {
if cols == nil {
return nil
}
dtos := make([]*ColumnDto, len(cols))
for i, col := range cols {
dtos[i] = ToDto(col)
}
return dtos
}
// ToListResp 转换为列表响应
func ToListResp(total int64, page, pageSize int, cols []*column.Column) *ColumnListResp {
return &ColumnListResp{
Total: total,
List: ToDtoList(cols),
Page: page,
PageSize: pageSize,
}
}

@ -0,0 +1,55 @@
package column
import "time"
// ColumnDto 专栏数据传输对象
type ColumnDto struct {
ID uint64 `json:"id"`
Title string `json:"title"` // 专栏标题
Brief string `json:"brief"` // 专栏简介
Cover string `json:"cover"` // 封面图片
AuthorID uint64 `json:"authorId"` // 作者ID
Status int8 `json:"status"` // 状态
ArticleNum int `json:"articleNum"` // 文章数量
FollowNum int `json:"followNum"` // 关注人数
PurchaseNum int `json:"purchaseNum"` // 购买人数
Unlock bool `json:"unlock"` //是否已经解锁
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// CreateColumnReq 创建专栏请求
type CreateColumnReq struct {
Title string `json:"title" binding:"required"` // 专栏标题
Brief string `json:"brief" binding:"required"` // 专栏简介
Cover string `json:"cover" binding:"required"` // 封面图片
AuthorID uint64 `json:"authorId" binding:"required"` // 作者ID
}
// UpdateColumnReq 更新专栏请求
type UpdateColumnReq struct {
ID uint64 `json:"id" binding:"required"` // 专栏ID
Title string `json:"title" binding:"required"` // 专栏标题
Brief string `json:"brief" binding:"required"` // 专栏简介
Cover string `json:"cover" binding:"required"` // 封面图片
}
// UpdateStatusReq 更新状态请求
type UpdateStatusReq struct {
ID uint64 `json:"id" binding:"required"` // 专栏ID
Status int8 `json:"status" binding:"required"` // 状态
}
// ColumnListReq 专栏列表请求
type ColumnListReq struct {
Page int `json:"page" binding:"required"` // 页码
PageSize int `json:"pageSize" binding:"required"` // 每页数量
AuthorID uint64 `json:"authorId"` // 作者ID可选
}
// ColumnListResp 专栏列表响应
type ColumnListResp struct {
Total int64 `json:"total"` // 总数
List []*ColumnDto `json:"list"` // 列表数据
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页数量
}

@ -0,0 +1,205 @@
package column
import (
"cls/internal/domain/column"
"cls/internal/domain/purchase"
"cls/internal/domain/user"
"cls/pkg/logger"
"cls/pkg/util/page"
"cls/pkg/web"
"errors"
)
var (
ErrInvalidTitle = errors.New("标题不能为空")
ErrNotFound = errors.New("专栏不存在")
ErrInvalidPage = errors.New("无效的分页参数")
)
// Service 专栏应用服务
type Service struct {
repo column.ColumnRepository
purchaseRepo purchase.Repository
userRepo user.UserRepository
log logger.Logger
}
// NewService 创建专栏服务
func NewService(repo column.ColumnRepository, purchaseRepo purchase.Repository, userRepo user.UserRepository, log logger.New) *Service {
return &Service{
repo: repo,
purchaseRepo: purchaseRepo,
userRepo: userRepo,
log: log("cls:service:column"),
}
}
// CreateColumn 创建专栏
func (s *Service) CreateColumn(req *CreateColumnReq) (*ColumnDto, error) {
if req.Title == "" {
return nil, ErrInvalidTitle
}
col := column.NewColumn(req.Title, req.Brief, req.Cover, req.AuthorID)
if err := s.repo.Save(col); err != nil {
s.log.Error("failed to save column", "error", err)
return nil, err
}
return ToDto(col), nil
}
// GetColumn 获取专栏信息
func (s *Service) GetColumn(ePhone string, name string) (*ColumnDto, error) {
col, err := s.repo.FindByName(name)
if err != nil {
s.log.Error("failed to find column", "error", err)
return nil, err
}
columnDto := &ColumnDto{
ID: col.ID,
Title: col.Title,
Brief: col.Brief,
Cover: col.Brief,
Unlock: false,
}
if ePhone == "" {
return columnDto, nil
}
userData, err := s.userRepo.FindByPhone(ePhone)
if err != nil {
s.log.Error(err.Error())
return columnDto, nil
}
purchaseData, err := s.purchaseRepo.FindColumnById(userData.Id, col.ID)
if err != nil {
s.log.Error(err)
return columnDto, nil
}
if len(purchaseData) != 0 {
if purchaseData[0].ContentId == col.ID {
columnDto.Unlock = true
}
}
return columnDto, nil
}
// GetAuthorColumns 获取作者的专栏列表
func (s *Service) GetAuthorColumns(authorID uint64) ([]*ColumnDto, error) {
cols, err := s.repo.FindByAuthorID(authorID)
if err != nil {
s.log.Error("failed to find author columns", "error", err)
return nil, err
}
return ToDtoList(cols), nil
}
// GetColumnList 获取专栏列表
func (s *Service) GetColumnList(p *page.Page, params map[string]string) error {
conds := web.ParseFilters(params)
cols := make([]*column.Column, 0)
p.Content = &cols
if err := s.repo.FindAll(p, conds); err != nil {
s.log.Error("failed to find columns", "error", err)
return err
}
p.Content = ToDtoList(p.Content.([]*column.Column))
return nil
}
// UpdateColumn 更新专栏信息
func (s *Service) UpdateColumn(req *UpdateColumnReq) (*ColumnDto, error) {
if req.Title == "" {
return nil, ErrInvalidTitle
}
col, err := s.repo.FindByID(req.ID)
if err != nil {
s.log.Error("failed to find column", "error", err)
return nil, err
}
if col == nil {
return nil, ErrNotFound
}
col.Title = req.Title
col.Brief = req.Brief
col.Cover = req.Cover
if err := s.repo.Update(col); err != nil {
s.log.Error("failed to update column", "error", err)
return nil, err
}
return ToDto(col), nil
}
// DeleteColumn 删除专栏
func (s *Service) DeleteColumn(id uint64) error {
return s.repo.Delete(id)
}
// UpdateColumnStatus 更新专栏状态
func (s *Service) UpdateColumnStatus(req *UpdateStatusReq) (*ColumnDto, error) {
col, err := s.repo.FindByID(req.ID)
if err != nil {
s.log.Error("failed to find column", "error", err)
return nil, err
}
if col == nil {
return nil, ErrNotFound
}
col.Status = req.Status
if err := s.repo.Update(col); err != nil {
s.log.Error("failed to update column status", "error", err)
return nil, err
}
return ToDto(col), nil
}
// UpdateFollowNum 更新关注人数
func (s *Service) UpdateFollowNum(id uint64, isAdd bool) error {
col, err := s.repo.FindByID(id)
if err != nil {
s.log.Error("failed to find column", "error", err)
return err
}
if col == nil {
return ErrNotFound
}
if isAdd {
col.AddFollow()
} else {
col.RemoveFollow()
}
if err := s.repo.Update(col); err != nil {
s.log.Error("failed to update column follow num", "error", err)
return err
}
return nil
}
// UpdatePurchaseNum 更新购买人数
func (s *Service) UpdatePurchaseNum(id uint64, isAdd bool) error {
col, err := s.repo.FindByID(id)
if err != nil {
s.log.Error("failed to find column", "error", err)
return err
}
if col == nil {
return ErrNotFound
}
if isAdd {
col.AddPurchase()
} else {
col.RemovePurchase()
}
if err := s.repo.Update(col); err != nil {
s.log.Error("failed to update column purchase num", "error", err)
return err
}
return nil
}

@ -0,0 +1,59 @@
package coupon
import (
"cls/internal/domain/coupon"
"time"
)
type CouponDto struct {
Id uint64 `json:"id"`
Name string `json:"name"`
Type coupon.CouponType `json:"type"`
Value int64 `json:"value"`
MinAmount int64 `json:"minAmount"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Status coupon.CouponStatus `json:"status"`
AdminId uint64 `json:"adminId"`
UserId uint64 `json:"userId"`
}
// CreateCouponRequest 创建优惠券请求
type CreateCouponRequest struct {
Name string `json:"name"` // 优惠券名称
Type int `json:"type"` // 优惠券类型
Value int64 `json:"value"` // 优惠券值
MinAmount int64 `json:"minAmount"` // 最低使用金额
StartTime time.Time `json:"startTime"` // 开始时间
EndTime time.Time `json:"endTime"` // 结束时间
TotalCount int `json:"totalCount"` // 总数量
}
// GetUserCouponsResponse 获取用户优惠券响应
type GetUserCouponsResponse struct {
ID uint64 `json:"id"` // 优惠券ID
Code string `json:"code"` // 优惠券码
Name string `json:"name"` // 优惠券名称
Type int `json:"type"` // 优惠券类型
Value int64 `json:"value"` // 优惠券值
MinAmount int64 `json:"minAmount"` // 最低使用金额
StartTime time.Time `json:"startTime"` // 开始时间
EndTime time.Time `json:"endTime"` // 结束时间
Status int `json:"status"` // 优惠券状态
UsedAt *time.Time `json:"usedAt"` // 使用时间
}
// GetCouponResponse 获取优惠券响应
type GetCouponResponse struct {
ID uint64 `json:"id"` // 优惠券ID
Code string `json:"code"` // 优惠券码
Name string `json:"name"` // 优惠券名称
Type int `json:"type"` // 优惠券类型
Value int64 `json:"value"` // 优惠券值
MinAmount int64 `json:"minAmount"` // 最低使用金额
StartTime time.Time `json:"startTime"` // 开始时间
EndTime time.Time `json:"endTime"` // 结束时间
TotalCount int `json:"totalCount"` // 总数量
UsedCount int `json:"usedCount"` // 已使用数量
Status int `json:"status"` // 优惠券状态
}

@ -0,0 +1,84 @@
package coupon
import (
"cls/internal/domain/coupon"
"cls/internal/domain/user"
"cls/pkg/logger"
)
// CouponService 优惠券应用服务
type CouponService struct {
repo coupon.CouponRepository
userRepo user.UserRepository
log logger.Logger
}
// NewCouponService 创建优惠券应用服务
func NewCouponService(repo coupon.CouponRepository, userRepo user.UserRepository, log logger.New) *CouponService {
return &CouponService{
repo: repo,
userRepo: userRepo,
log: log("cls:service:coupon"),
}
}
// CreateCoupon 创建优惠券
func (s *CouponService) CreateCoupon(req *CouponDto) error {
_, err := s.userRepo.FindByID(req.UserId)
if err != nil {
s.log.Error(err)
return err
}
coupon := &coupon.Coupon{
Name: req.Name,
Type: req.Type,
Value: req.Value,
MinAmount: req.MinAmount,
StartTime: req.StartTime,
EndTime: req.EndTime,
AdminID: req.AdminId,
UserID: req.UserId,
Status: coupon.CouponStatusNormal,
}
if err := s.repo.Create(coupon); err != nil {
s.log.Error(err)
return err
}
return nil
}
// IssueCouponRequest 发放优惠券请求
type IssueCouponRequest struct {
CouponID uint64 `json:"couponId"` // 优惠券ID
UserID uint64 `json:"userId"` // 用户ID
}
// GetUserCoupons 获取用户的优惠券列表
func (s *CouponService) GetUserCoupons(ePhone string) ([]*CouponDto, error) {
user, err := s.userRepo.FindByPhone(ePhone)
if err != nil {
s.log.Error(err)
return nil, err
}
coupons, err := s.repo.ListByUserID(user.Id)
if err != nil {
s.log.Error(err)
return nil, err
}
var resp []*CouponDto
for _, c := range coupons {
if c.Status == coupon.CouponStatusNormal {
resp = append(resp, &CouponDto{
Id: c.ID,
Name: c.Name,
Type: c.Type,
Value: c.Value,
MinAmount: c.MinAmount,
StartTime: c.StartTime,
EndTime: c.EndTime,
Status: c.Status,
})
}
}
return resp, nil
}

@ -0,0 +1,40 @@
package order
import (
"cls/internal/domain/order"
"errors"
"time"
)
var (
ErrInvalidAmount = errors.New("价格不能小于0")
ErrOrderExists = errors.New("订单已存在")
ErrOrderNotFound = errors.New("订单不存在")
ErrOrderPaid = errors.New("订单已支付")
ErrOrderCanceled = errors.New("订单已取消")
)
// CreateOrderRequest 创建订单请求
type CreateOrderRequest struct {
UserID uint64 `json:"userId"` // 用户ID
TargetID uint64 `json:"targetId"` // 商品ID
Type order.OrderType `json:"type"` // 订单类型
Amount int64 `json:"amount"` // 订单金额
Duration int `json:"duration"` // 购买时长
Description string `json:"description"` // 商品描述
}
// OrderResponse 订单响应
type OrderResponse struct {
ID uint64 `json:"id"` // 订单ID
OrderNo string `json:"orderNo"` // 订单编号
UserID uint64 `json:"userId"` // 用户ID
TargetID uint64 `json:"targetId"` // 商品ID
Type order.OrderType `json:"type"` // 订单类型
Amount int64 `json:"amount"` // 订单金额
Duration int `json:"duration"` // 购买时长
Status order.OrderStatus `json:"status"` // 订单状态
Description string `json:"description"` // 商品描述
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
}

@ -0,0 +1,88 @@
package order
import (
dto "cls/internal/application/payment"
"cls/internal/domain/order"
"cls/internal/domain/payment"
"cls/pkg/logger"
"fmt"
"time"
)
// OrderService 订单应用服务
type OrderService struct {
repo order.AggregateRepository
log logger.Logger
}
// NewOrderService 创建订单应用服务
func NewOrderService(repo order.AggregateRepository, log logger.New) *OrderService {
return &OrderService{
repo: repo,
log: log("cls:service:order"),
}
}
// CreateOrder 创建订单
func (s *OrderService) CreateOrder(req *CreateOrderRequest) (*OrderResponse, error) {
// 创建订单
o := &order.Order{
OrderNo: generateOrderNo(),
UserID: req.UserID,
TargetID: req.TargetID,
Type: req.Type,
Amount: req.Amount,
Duration: req.Duration,
Status: order.OrderStatusPending,
Description: req.Description,
}
// 创建聚合根
aggregate := order.NewOrderAggregate(o)
// 保存聚合根
if err := s.repo.Save(aggregate); err != nil {
s.log.Error(err)
return nil, err
}
return nil, nil
}
// CreatePayment 创建支付订单
func (s *OrderService) CreatePayment(orderNo string, paymentType payment.PaymentType) (*dto.PaymentResponse, error) {
// 获取订单聚合根
aggregate, err := s.repo.GetByOrderNo(orderNo)
if err != nil {
s.log.Error(err)
return nil, err
}
// 创建支付订单
if err := aggregate.CreatePayment(paymentType); err != nil {
return nil, err
}
// 保存聚合根
if err := s.repo.Save(aggregate); err != nil {
s.log.Error(err)
return nil, err
}
return dto.FromEntity(aggregate.Payment), nil
}
// DeleteOrder 删除订单
func (s *OrderService) DeleteOrder(id uint64) error {
if err := s.repo.Delete(id); err != nil {
s.log.Error(err)
return err
}
return nil
}
// generateOrderNo 生成订单号
func generateOrderNo() string {
return fmt.Sprintf("%d%d", time.Now().UnixNano()/1e6, time.Now().Unix()%1000)
}

@ -0,0 +1,40 @@
package payment
import (
"cls/internal/domain/payment"
"time"
)
// PaymentResponse 支付订单响应
type PaymentResponse struct {
ID uint64 `json:"id"` // 支付ID
OrderNo string `json:"orderNo"` // 订单编号
TransactionID string `json:"transactionId"` // 第三方交易号
UserID uint64 `json:"userId"` // 用户ID
TargetID uint64 `json:"targetId"` // 商品ID
Type payment.PaymentType `json:"type"` // 支付类型
Amount int64 `json:"amount"` // 支付金额
Status payment.PaymentStatus `json:"status"` // 支付状态
Description string `json:"description"` // 支付描述
NotifyData string `json:"notifyData"` // 支付回调数据
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
}
// FromEntity 从实体转换为响应
func FromEntity(e *payment.Payment) *PaymentResponse {
return &PaymentResponse{
ID: e.ID,
OrderNo: e.OrderNo,
TransactionID: e.TransactionID,
UserID: e.UserID,
TargetID: e.TargetID,
Type: e.Type,
Amount: e.Amount,
Status: e.Status,
Description: e.Description,
NotifyData: e.NotifyData,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}

@ -4,10 +4,10 @@ import (
"cls/internal/domain/payment"
"cls/pkg/logger"
"cls/pkg/util/page"
"cls/pkg/web"
"errors"
"fmt"
"time"
"xorm.io/builder"
)
var (
@ -44,7 +44,7 @@ func (s *PaymentService) CreateOrder(userID, targetID uint64, paymentType paymen
orderNo := fmt.Sprintf("%d%d%d", time.Now().UnixNano(), userID, targetID)
// 检查订单是否已存在
_, err := s.repo.FindByOrderNo(orderNo)
_, err := s.repo.GetByOrderNo(orderNo)
if err == nil {
return nil, ErrOrderExists
}
@ -56,12 +56,12 @@ func (s *PaymentService) CreateOrder(userID, targetID uint64, paymentType paymen
TargetID: targetID,
Type: paymentType,
Amount: amount,
Status: payment.StatusPending,
Status: payment.PaymentStatusPending,
Description: description,
}
// 保存订单
if err := s.repo.Save(payment); err != nil {
if err := s.repo.Create(payment); err != nil {
return nil, err
}
@ -70,28 +70,30 @@ func (s *PaymentService) CreateOrder(userID, targetID uint64, paymentType paymen
// GetOrder 获取支付订单
func (s *PaymentService) GetOrder(orderNo string) (*payment.Payment, error) {
return s.repo.FindByOrderNo(orderNo)
return s.repo.GetByOrderNo(orderNo)
}
// GetUserOrders 获取用户支付订单列表
func (s *PaymentService) GetUserOrders(userID uint64, page *page.Page) error {
return s.repo.FindByUserID(userID, page)
func (s *PaymentService) GetUserOrders(userID uint64, page *page.Page, params map[string]string) error {
conds := web.ParseFilters(params)
return s.repo.ListByUserID(userID, page, conds)
}
// GetOrderList 获取支付订单列表
func (s *PaymentService) GetOrderList(page *page.Page, conds []builder.Cond) error {
func (s *PaymentService) GetOrderList(page *page.Page, params map[string]string) error {
conds := web.ParseFilters(params)
return s.repo.FindAll(page, conds)
}
// UpdateOrderStatus 更新订单状态
func (s *PaymentService) UpdateOrderStatus(orderNo string, status payment.PaymentStatus, transactionID string, notifyData string) error {
order, err := s.repo.FindByOrderNo(orderNo)
order, err := s.repo.GetByOrderNo(orderNo)
if err != nil {
return ErrOrderNotFound
}
// 检查订单状态
if order.Status != payment.StatusPending {
if order.Status != payment.PaymentStatusPending {
return ErrOrderPaid
}
@ -105,18 +107,18 @@ func (s *PaymentService) UpdateOrderStatus(orderNo string, status payment.Paymen
// CancelOrder 取消支付订单
func (s *PaymentService) CancelOrder(orderNo string) error {
order, err := s.repo.FindByOrderNo(orderNo)
order, err := s.repo.GetByOrderNo(orderNo)
if err != nil {
return ErrOrderNotFound
}
// 检查订单状态
if order.Status != payment.StatusPending {
if order.Status != payment.PaymentStatusFailed {
return ErrOrderPaid
}
// 更新订单状态为已取消
order.Status = payment.StatusCancelled
order.Status = payment.PaymentStatusRefunded
return s.repo.Update(order)
}
@ -124,3 +126,5 @@ func (s *PaymentService) CancelOrder(orderNo string) error {
func (s *PaymentService) DeleteOrder(id uint64) error {
return s.repo.Delete(id)
}
// CreatePayment 创建支付订单

@ -1,147 +0,0 @@
package payment
import (
"cls/internal/domain/payment"
"cls/pkg/logger"
"cls/pkg/util/page"
"errors"
"fmt"
"time"
"xorm.io/builder"
)
// ServiceImpl 支付服务实现
type ServiceImpl struct {
repo payment.PaymentRepository
provider payment.PaymentProvider
log logger.Logger
}
// NewServiceImpl 创建支付服务实现
func NewServiceImpl(repo payment.PaymentRepository, provider payment.PaymentProvider, log logger.New) *ServiceImpl {
return &ServiceImpl{
repo: repo,
provider: provider,
log: log("cls:service:payment"),
}
}
// CreateOrder 创建支付订单
func (s *ServiceImpl) CreateOrder(userID, targetID uint64, paymentType payment.PaymentType, amount int64, description string) (*payment.Payment, error) {
if amount <= 0 {
return nil, ErrInvalidAmount
}
// 生成订单号
orderNo := fmt.Sprintf("%d%d%d", time.Now().UnixNano(), userID, targetID)
// 检查订单是否已存在
_, err := s.repo.FindByOrderNo(orderNo)
if err == nil {
return nil, ErrOrderExists
}
// 创建支付订单
payment := &payment.Payment{
OrderNo: orderNo,
UserID: userID,
TargetID: targetID,
Type: paymentType,
Amount: amount,
Status: payment.StatusPending,
Description: description,
}
// 保存订单
if err := s.repo.Save(payment); err != nil {
return nil, err
}
return payment, nil
}
// GetOrder 获取支付订单
func (s *ServiceImpl) GetOrder(orderNo string) (*payment.Payment, error) {
return s.repo.FindByOrderNo(orderNo)
}
// GetUserOrders 获取用户支付订单列表
func (s *ServiceImpl) GetUserOrders(userID uint64, page *page.Page) error {
return s.repo.FindByUserID(userID, page)
}
// GetOrderList 获取支付订单列表
func (s *ServiceImpl) GetOrderList(page *page.Page, conds []builder.Cond) error {
return s.repo.FindAll(page, conds)
}
// UpdateOrderStatus 更新订单状态
func (s *ServiceImpl) UpdateOrderStatus(orderNo string, status payment.PaymentStatus, transactionID string, notifyData string) error {
order, err := s.repo.FindByOrderNo(orderNo)
if err != nil {
return ErrOrderNotFound
}
// 检查订单状态
if order.Status != payment.StatusPending {
return ErrOrderPaid
}
// 更新订单状态
order.Status = status
order.TransactionID = transactionID
order.NotifyData = notifyData
return s.repo.Update(order)
}
// CancelOrder 取消支付订单
func (s *ServiceImpl) CancelOrder(orderNo string) error {
order, err := s.repo.FindByOrderNo(orderNo)
if err != nil {
return ErrOrderNotFound
}
// 检查订单状态
if order.Status != payment.StatusPending {
return ErrOrderPaid
}
// 更新订单状态为已取消
order.Status = payment.StatusCancelled
return s.repo.Update(order)
}
// DeleteOrder 删除支付订单
func (s *ServiceImpl) DeleteOrder(id uint64) error {
return s.repo.Delete(id)
}
// GetPaymentURL 获取支付链接
func (s *ServiceImpl) GetPaymentURL(order *payment.Payment) (map[string]interface{}, error) {
return s.provider.CreatePayment(order)
}
// HandleNotify 处理支付回调通知
func (s *ServiceImpl) HandleNotify(notifyData string) error {
// 验证通知数据
result, err := s.provider.VerifyNotify(notifyData)
if err != nil {
return fmt.Errorf("验证通知数据失败: %v", err)
}
// 获取订单号
orderNo := result["order_no"]
if orderNo == "" {
return errors.New("订单号不能为空")
}
// 获取订单
order, err := s.repo.FindByOrderNo(orderNo)
if err != nil {
return fmt.Errorf("获取订单失败: %v", err)
}
// 更新订单状态
return s.UpdateOrderStatus(order.OrderNo, payment.StatusSuccess, result["transaction_id"], notifyData)
}

@ -0,0 +1,43 @@
package price
import (
"cls/internal/domain/price"
"errors"
)
type PriceDto struct {
ID uint64 `json:"id" omitempty`
TargetID uint64 `json:"targetID" omitempty`
Type price.PriceType `json:"type" omitempty`
Amount int64 `json:"amount" omitempty`
OneMonthPrice int64 `json:"oneMonthPrice" omitempty`
ThreeMonthsPrice int64 `json:"threeMonthsPrice" omitempty`
SixMonthsPrice int64 `json:"sixMonthsPrice" omitempty`
OneYearPrice int64 `json:"oneYearPrice" omitempty`
Discount float32 `json:"discount" omitempty`
AdminID uint64 `json:"adminID" omitempty`
}
func (p *PriceDto) ToPrice() *price.Price {
return &price.Price{
ID: p.ID,
TargetID: p.TargetID,
Type: p.Type,
Amount: p.Amount,
OneMonthPrice: p.OneMonthPrice,
ThreeMonthsPrice: p.ThreeMonthsPrice,
SixMonthsPrice: p.SixMonthsPrice,
OneYearPrice: p.OneYearPrice,
Discount: p.Discount,
AdminID: p.AdminID,
}
}
func (p *PriceDto) Validate() error {
if p.Type == price.TypeArticle {
if p.Amount <= 0 {
return errors.New("文章价格必须大于0")
}
}
return nil
}

@ -4,13 +4,14 @@ import (
"cls/internal/domain/price"
"cls/pkg/logger"
"cls/pkg/util/page"
"cls/pkg/web"
"errors"
"xorm.io/builder"
)
var (
ErrInvalidAmount = errors.New("价格不能小于0")
ErrInvalidType = errors.New("无效的价格类型")
ErrInvalidAmount = errors.New("价格不能小于0")
ErrInvalidType = errors.New("无效的价格类型")
ErrInvalidDuration = errors.New("无效的订阅时长")
)
// PriceService 价格管理服务
@ -20,7 +21,7 @@ type PriceService struct {
}
// NewService 创建价格管理服务
func NewService(repo price.PriceRepository, log logger.New) *PriceService {
func NewPriceService(repo price.PriceRepository, log logger.New) *PriceService {
return &PriceService{
repo: repo,
log: log("cls:service:price"),
@ -28,64 +29,91 @@ func NewService(repo price.PriceRepository, log logger.New) *PriceService {
}
// SetPrice 设置价格
func (s *PriceService) SetPrice(targetID uint64, priceType price.PriceType, amount int64, adminID uint64) error {
if amount < 0 {
func (s *PriceService) SetPrice(dto *PriceDto) error {
if dto.Amount < 0 {
return ErrInvalidAmount
}
// 检查是否已存在价格记录
existingPrice, err := s.repo.FindByTargetID(targetID, priceType)
existingPrice, err := s.repo.FindByTargetID(dto.TargetID, dto.Type)
if err != nil {
// 如果记录不存在,创建新记录
newPrice := &price.Price{
TargetID: targetID,
Type: priceType,
Amount: amount,
AdminID: adminID,
}
newPrice := dto.ToPrice()
return s.repo.Save(newPrice)
}
// 如果记录存在,更新价格
existingPrice.Amount = amount
existingPrice.AdminID = adminID
existingPrice.Amount = dto.Amount
existingPrice.OneMonthPrice = dto.OneMonthPrice
existingPrice.ThreeMonthsPrice = dto.ThreeMonthsPrice
existingPrice.SixMonthsPrice = dto.SixMonthsPrice
existingPrice.OneYearPrice = dto.OneYearPrice
existingPrice.AdminID = dto.AdminID
return s.repo.Update(existingPrice)
}
func (s *PriceService) GetArticlePrice(dto *PriceDto) (*PriceDto, error) {
dto.Type = price.TypeArticle
return s.GetPrice(dto)
}
func (s *PriceService) GetColumnPrice(dto *PriceDto) (*PriceDto, error) {
dto.Type = price.TypeColumn
return s.GetPrice(dto)
}
// GetPrice 获取价格(如果不存在则使用默认价格)
func (s *PriceService) GetPrice(targetID uint64, priceType price.PriceType) (int64, error) {
func (s *PriceService) GetPrice(dto *PriceDto) (*PriceDto, error) {
// 检查是否已存在价格记录
existingPrice, err := s.repo.FindByTargetID(targetID, priceType)
existingPrice, err := s.repo.FindByTargetID(dto.TargetID, dto.Type)
if err != nil {
// 如果记录不存在,使用默认价格
var defaultAmount int64
switch priceType {
switch dto.Type {
case price.TypeArticle:
defaultAmount = price.DefaultArticlePrice
case price.TypeColumn:
defaultAmount = price.DefaultColumnPrice
default:
return 0, ErrInvalidType
return nil, ErrInvalidType
}
// 创建默认价格记录
newPrice := &price.Price{
TargetID: targetID,
Type: priceType,
Amount: defaultAmount,
AdminID: 0, // 系统默认价格
}
newPrice := price.NewPrice(dto.TargetID, dto.Type, defaultAmount, 0)
if err := s.repo.Save(newPrice); err != nil {
return 0, err
return nil, err
}
if dto.Type == price.TypeArticle {
return &PriceDto{
Amount: newPrice.Amount,
Discount: newPrice.Discount,
}, nil
}
return defaultAmount, nil
return &PriceDto{
OneMonthPrice: newPrice.OneMonthPrice,
ThreeMonthsPrice: newPrice.ThreeMonthsPrice,
SixMonthsPrice: newPrice.SixMonthsPrice,
OneYearPrice: newPrice.OneYearPrice,
Discount: 0.3,
}, nil
}
priceDto := &PriceDto{
Discount: existingPrice.Discount,
}
if dto.Type == price.TypeArticle {
priceDto.Amount = existingPrice.Amount
} else {
priceDto.OneMonthPrice = existingPrice.OneMonthPrice
priceDto.ThreeMonthsPrice = existingPrice.ThreeMonthsPrice
priceDto.SixMonthsPrice = existingPrice.SixMonthsPrice
priceDto.OneYearPrice = existingPrice.OneYearPrice
}
return existingPrice.Amount, nil
return priceDto, nil
}
// GetPriceList 获取价格列表
func (s *PriceService) GetPriceList(page *page.Page, conds []builder.Cond) error {
func (s *PriceService) GetPriceList(page *page.Page, params map[string]string) error {
conds := web.ParseFilters(params)
return s.repo.FindAll(page, conds)
}
@ -93,3 +121,20 @@ func (s *PriceService) GetPriceList(page *page.Page, conds []builder.Cond) error
func (s *PriceService) DeletePrice(id uint64) error {
return s.repo.Delete(id)
}
func (s *PriceService) UpdatePrice(dto *PriceDto) error {
if err := dto.Validate(); err != nil {
return err
}
existingPrice, err := s.repo.FindByTargetID(dto.TargetID, dto.Type)
if err != nil {
return err
}
existingPrice.Amount = dto.Amount
existingPrice.OneMonthPrice = dto.OneMonthPrice
existingPrice.ThreeMonthsPrice = dto.ThreeMonthsPrice
existingPrice.SixMonthsPrice = dto.SixMonthsPrice
existingPrice.OneYearPrice = dto.OneYearPrice
existingPrice.AdminID = dto.AdminID
return s.repo.Update(existingPrice)
}

@ -48,6 +48,6 @@ func (s *Service) CreatePurchase(userId, contentId uint64, contentType purchase.
}
// 创建购买记录
purchase := purchase.NewPurchase(userId, contentId, contentType, price)
purchase := purchase.NewPurchase(userId, contentId, contentType, price, 0)
return s.repo.Save(purchase)
}

@ -3,7 +3,7 @@ package article
type LianV1Article struct {
Id uint64 `xorm:"<- not null" ` //id
Title string `xorm:"<- not null" json:"title"` //标题
Type int64 `xorm:"<- not null"` //类型
Type uint64 `xorm:"<- not null"` //类型
Ctime int64 `xorm:"<- not null ctime" ` // 发布时间
Brief string `xorm:"<- not null" json:"brief"` //简介
Content string `xorm:"<- not null" json:"content"` //内容
@ -37,7 +37,7 @@ func WithArticleTitle(arg string) articleConfiguration {
}
}
func WithArticleType(arg int64) articleConfiguration {
func WithArticleType(arg uint64) articleConfiguration {
return func(article *LianV1Article) {
article.Type = arg
}

@ -0,0 +1,71 @@
package column
import "time"
// Column 专栏实体
type Column struct {
ID uint64 `xorm:"pk autoincr 'id'"`
Title string `xorm:"varchar(100) notnull 'title'"` // 专栏标题
Brief string `xorm:"varchar(500) 'brief'"` // 专栏简介
Cover string `xorm:"varchar(255) 'cover'"` // 封面图片
AuthorID uint64 `xorm:"notnull 'author_id'"` // 作者ID
Status int8 `xorm:"tinyint(1) notnull default 1 'status'"` // 状态1-正常 2-下架
ArticleNum int `xorm:"int default 0 'article_num'"` // 文章数量
FollowNum int `xorm:"int default 0 'follow_num'"` // 关注人数
PurchaseNum int `xorm:"int default 0 'purchase_num'"` // 购买人数
CreatedAt time.Time `xorm:"created 'created_at'"`
UpdatedAt time.Time `xorm:"updated 'updated_at'"`
DeletedAt *time.Time `xorm:"deleted 'deleted_at'"`
}
// NewColumn 创建专栏
func NewColumn(title string, brief string, cover string, authorID uint64) *Column {
return &Column{
Title: title,
Brief: brief,
Cover: cover,
AuthorID: authorID,
Status: 1,
}
}
// IsValid 检查专栏是否有效
func (c *Column) IsValid() bool {
return c.Status == 1 && c.DeletedAt == nil
}
// AddArticle 增加文章数量
func (c *Column) AddArticle() {
c.ArticleNum++
}
// RemoveArticle 减少文章数量
func (c *Column) RemoveArticle() {
if c.ArticleNum > 0 {
c.ArticleNum--
}
}
// AddFollow 增加关注人数
func (c *Column) AddFollow() {
c.FollowNum++
}
// RemoveFollow 减少关注人数
func (c *Column) RemoveFollow() {
if c.FollowNum > 0 {
c.FollowNum--
}
}
// AddPurchase 增加购买人数
func (c *Column) AddPurchase() {
c.PurchaseNum++
}
// RemovePurchase 减少购买人数
func (c *Column) RemovePurchase() {
if c.PurchaseNum > 0 {
c.PurchaseNum--
}
}

@ -0,0 +1,30 @@
package column
import (
"cls/pkg/util/page"
"xorm.io/builder"
)
// Repository 专栏仓储接口
type ColumnRepository interface {
// Save 保存专栏
Save(column *Column) error
// FindByID 根据ID查找专栏
FindByID(id uint64) (*Column, error)
// FindByName 根据名称查找专栏
FindByName(name string) (*Column, error)
// FindByAuthorID 查找作者的所有专栏
FindByAuthorID(authorID uint64) ([]*Column, error)
// FindAll 查询专栏列表
FindAll(page *page.Page, conds []builder.Cond) error
// Update 更新专栏
Update(column *Column) error
// Delete 删除专栏
Delete(id uint64) error
}

@ -0,0 +1,67 @@
package coupon
import "time"
// CouponType 优惠券类型
type CouponType int
const (
CouponTypeFullReduction CouponType = 1 // 满减
CouponTypeDiscount CouponType = 2 // 折扣
)
// CouponStatus 优惠券状态
type CouponStatus int
const (
CouponStatusNormal CouponStatus = 1 // 正常
CouponStatusUsed CouponStatus = 2 // 已使用
CouponStatusExpired CouponStatus = 3 // 已过期
CouponStatusDisabled CouponStatus = 4 // 已停用
)
// Coupon 优惠券实体
type Coupon struct {
ID uint64 `json:"id"` // 优惠券ID
Code string `json:"code"` // 优惠券码
Name string `json:"name"` // 优惠券名称
Type CouponType `json:"type"` // 优惠券类型
Value int64 `json:"value"` // 优惠券值
MinAmount int64 `json:"minAmount"` // 最低使用金额
StartTime time.Time `json:"startTime"` // 开始时间
EndTime time.Time `json:"endTime"` // 结束时间
Status CouponStatus `json:"status"` // 优惠券状态
AdminID uint64 `json:"adminId"` // 创建者ID
UserID uint64 `json:"userId"` // 使用者ID
UsedAt *time.Time `json:"usedAt"` // 使用时间
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt *time.Time `json:"deletedAt"` // 删除时间
}
// TableName 指定表名
// IsValid 检查优惠券是否有效
func (c *Coupon) IsValid() bool {
return c.ID > 0 && c.Code != "" && c.Name != "" && c.Status == CouponStatusNormal
}
// IsExpired 检查优惠券是否已过期
func (c *Coupon) IsExpired() bool {
return time.Now().After(c.EndTime)
}
// IsNotStarted 检查优惠券是否未开始
func (c *Coupon) IsNotStarted() bool {
return time.Now().Before(c.StartTime)
}
// IsUsed 检查优惠券是否已使用
func (c *Coupon) IsUsed() bool {
return c.Status == CouponStatusUsed
}
// IsDisabled 检查优惠券是否已停用
func (c *Coupon) IsDisabled() bool {
return c.Status == CouponStatusDisabled
}

@ -0,0 +1,25 @@
package coupon
// CouponRepository 优惠券仓储接口
type CouponRepository interface {
// Create 创建优惠券
Create(coupon *Coupon) error
// GetByID 根据ID获取优惠券
GetByID(id uint64) (*Coupon, error)
// GetByCode 根据优惠券码获取优惠券
GetByCode(code string) (*Coupon, error)
// List 获取优惠券列表
List(page, size int) ([]*Coupon, int64, error)
// ListByUserID 获取用户的优惠券列表
ListByUserID(userID uint64) ([]*Coupon, error)
// Update 更新优惠券
Update(coupon *Coupon) error
// Delete 删除优惠券
Delete(id uint64) error
}

@ -0,0 +1,131 @@
package order
import (
"cls/internal/domain/payment"
"cls/internal/domain/purchase"
"errors"
"time"
)
// OrderAggregate 订单聚合根
type OrderAggregate struct {
Order *Order
Payment *payment.Payment
Purchase *purchase.Purchase
}
// NewOrderAggregate 创建订单聚合根
func NewOrderAggregate(order *Order) *OrderAggregate {
return &OrderAggregate{
Order: order,
}
}
// CreatePayment 创建支付订单
func (a *OrderAggregate) CreatePayment(paymentType payment.PaymentType) error {
if a.Order.Status != OrderStatusPending {
return payment.ErrInvalidTransactionID
}
a.Payment = payment.NewPayment(
a.Order.OrderNo,
a.Order.UserID,
a.Order.TargetID,
paymentType,
a.Order.Amount,
a.Order.Description,
)
return nil
}
// HandlePaymentSuccess 处理支付成功
func (a *OrderAggregate) HandlePaymentSuccess(transactionID string, notifyData string) error {
if a.Payment == nil {
return payment.ErrPaymentNotFound
}
if a.Payment.IsSuccess() {
return payment.ErrPaymentSuccess
}
// 更新支付状态
a.Payment.Status = payment.PaymentStatusSuccess
a.Payment.TransactionID = transactionID
a.Payment.NotifyData = notifyData
// 更新订单状态
a.Order.Status = OrderStatusPaid
// 创建购买记录
a.Purchase = &purchase.Purchase{
UserId: a.Order.UserID,
ContentId: a.Order.TargetID,
ContentType: purchase.ContentType(a.Order.Type),
Price: float64(a.Order.Amount) / 100, // 转换为元
Duration: a.Order.Duration,
ExpiredAt: time.Now().AddDate(0, a.Order.Duration, 0),
Status: 1, // 有效状态
}
return nil
}
// HandlePaymentFailed 处理支付失败
func (a *OrderAggregate) HandlePaymentFailed(notifyData string) error {
if a.Payment == nil {
return payment.ErrPaymentNotFound
}
if a.Payment.IsFailed() {
return payment.ErrPaymentFailed
}
// 更新支付状态
a.Payment.Status = payment.PaymentStatusFailed
a.Payment.NotifyData = notifyData
// 更新订单状态
a.Order.Status = OrderStatusCanceled
return nil
}
// HandleRefund 处理退款
func (a *OrderAggregate) HandleRefund() error {
if a.Payment == nil {
return payment.ErrPaymentNotFound
}
if !a.Payment.IsSuccess() {
return errors.New("支付订单未支付成功")
}
// 更新支付状态
a.Payment.Status = payment.PaymentStatusRefunded
// 更新订单状态
a.Order.Status = OrderStatusRefunded
// 更新购买记录状态
if a.Purchase != nil {
a.Purchase.Status = 0 // 失效状态
}
return nil
}
// IsValid 检查聚合根是否有效
func (a *OrderAggregate) IsValid() bool {
return a.Order != nil && a.Order.IsValid()
}
// IsPaid 检查是否已支付
func (a *OrderAggregate) IsPaid() bool {
return a.Order != nil && a.Order.IsPaid() && a.Payment != nil && a.Payment.IsSuccess()
}
// IsRefunded 检查是否已退款
func (a *OrderAggregate) IsRefunded() bool {
return a.Order != nil && a.Order.IsRefunded() && a.Payment != nil && a.Payment.IsRefunded()
}

@ -0,0 +1,17 @@
package order
import "cls/pkg/util/page"
// AggregateRepository 订单聚合根仓储接口
type AggregateRepository interface {
// Save 保存订单聚合根
Save(aggregate *OrderAggregate) error
// GetByOrderNo 根据订单号获取订单聚合根
GetByOrderNo(orderNo string) (*OrderAggregate, error)
// GetByID 根据ID获取订单聚合根
GetByID(id uint64) (*OrderAggregate, error)
// ListByUserID 获取用户订单聚合根列表
ListByUserID(userID uint64, page *page.Page) ([]*OrderAggregate, int64, error)
// Delete 删除订单聚合根
Delete(id uint64) error
}

@ -0,0 +1,59 @@
package order
import "time"
// OrderType 订单类型
type OrderType int
const (
OrderTypeArticle OrderType = iota + 1 // 文章订单
OrderTypeColumn // 专栏订单
)
// OrderStatus 订单状态
type OrderStatus int
const (
OrderStatusPending OrderStatus = iota + 1 // 待支付
OrderStatusPaid // 已支付
OrderStatusCanceled // 已取消
OrderStatusRefunded // 已退款
)
// Order 订单实体
type Order struct {
ID uint64 `gorm:"primarykey" json:"id"` // 订单ID
OrderNo string `gorm:"uniqueIndex" json:"orderNo"` // 订单编号
UserID uint64 `json:"userId"` // 用户ID
TargetID uint64 `json:"targetId"` // 商品ID文章ID或专栏ID
Type OrderType `json:"type"` // 订单类型(文章/专栏)
Amount int64 `json:"amount"` // 订单金额(分)
Duration int `json:"duration"` // 购买时长(月)
Status OrderStatus `json:"status"` // 订单状态
Description string `json:"description"` // 商品描述
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt *time.Time `gorm:"index" json:"deletedAt"` // 删除时间
}
// TableName 指定表名
// IsValid 检查订单是否有效
func (o *Order) IsValid() bool {
return o.ID > 0 && o.OrderNo != "" && o.UserID > 0 && o.TargetID > 0
}
// IsPaid 检查订单是否已支付
func (o *Order) IsPaid() bool {
return o.Status == OrderStatusPaid
}
// IsCanceled 检查订单是否已取消
func (o *Order) IsCanceled() bool {
return o.Status == OrderStatusCanceled
}
// IsRefunded 检查订单是否已退款
func (o *Order) IsRefunded() bool {
return o.Status == OrderStatusRefunded
}

@ -0,0 +1,25 @@
package order
// OrderRepository 订单仓储接口
type OrderRepository interface {
// Create 创建订单
Create(order *Order) error
// GetByID 根据ID获取订单
GetByID(id uint64) (*Order, error)
// GetByOrderNo 根据订单号获取订单
GetByOrderNo(orderNo string) (*Order, error)
// ListByUserID 获取用户的订单列表
ListByUserID(userID uint64, page, size int) ([]*Order, int64, error)
// Update 更新订单
Update(order *Order) error
// UpdateStatus 更新订单状态
UpdateStatus(id uint64, status OrderStatus) error
// Delete 删除订单
Delete(id uint64) error
}

@ -3,36 +3,71 @@ package payment
import "time"
// PaymentType 支付类型
type PaymentType int8
type PaymentType int
const (
TypeArticle PaymentType = 1 // 文章支付
TypeColumn PaymentType = 2 // 专栏支付
PaymentTypeWechat PaymentType = iota + 1 // 微信支付
PaymentTypeAlipay // 支付宝支付
)
// PaymentStatus 支付状态
type PaymentStatus int8
type PaymentStatus int
const (
StatusPending PaymentStatus = 0 // 待支付
StatusSuccess PaymentStatus = 1 // 支付成功
StatusFailed PaymentStatus = 2 // 支付失败
StatusCancelled PaymentStatus = 3 // 已取消
PaymentStatusPending PaymentStatus = iota + 1 // 待支付
PaymentStatusSuccess // 支付成功
PaymentStatusFailed // 支付失败
PaymentStatusRefunded // 已退款
)
// Payment 支付订单实体
type Payment struct {
ID uint64 `json:"id" xorm:"pk autoincr 'id'"`
OrderNo string `json:"order_no" xorm:"varchar(32) notnull unique 'order_no'"` // 商户订单号
TransactionID string `json:"transaction_id" xorm:"varchar(32) 'transaction_id'"` // 微信支付订单号
UserID uint64 `json:"user_id" xorm:"not null 'user_id'"` // 用户ID
TargetID uint64 `json:"target_id" xorm:"not null 'target_id'"` // 目标ID文章ID或专栏ID
Type PaymentType `json:"type" xorm:"not null 'type'"` // 支付类型
Amount int64 `json:"amount" xorm:"not null 'amount'"` // 支付金额(分)
Status PaymentStatus `json:"status" xorm:"not null default 0 'status'"` // 支付状态
Description string `json:"description" xorm:"varchar(255) 'description'"` // 商品描述
NotifyData string `json:"notify_data" xorm:"text 'notify_data'"` // 回调数据
CreatedAt time.Time `json:"created_at" xorm:"created 'created_at'"`
UpdatedAt time.Time `json:"updated_at" xorm:"updated 'updated_at'"`
DeletedAt *time.Time `json:"deleted_at" xorm:"deleted 'deleted_at'"`
ID uint64 `gorm:"primarykey" json:"id"` // 支付ID
OrderNo string `gorm:"uniqueIndex" json:"orderNo"` // 订单编号
TransactionID string `json:"transactionId"` // 第三方交易号
UserID uint64 `json:"userId"` // 用户ID
TargetID uint64 `json:"targetId"` // 商品ID
Type PaymentType `json:"type"` // 支付类型
Amount int64 `json:"amount"` // 支付金额
Status PaymentStatus `json:"status"` // 支付状态
Description string `json:"description"` // 支付描述
NotifyData string `json:"notifyData"` // 支付回调数据
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt *time.Time `gorm:"index" json:"deletedAt"` // 删除时间
}
// TableName 指定表名
// IsValid 检查支付订单是否有效
func (p *Payment) IsValid() bool {
return p.ID > 0 && p.OrderNo != "" && p.UserID > 0 && p.TargetID > 0
}
// IsSuccess 检查支付是否成功
func (p *Payment) IsSuccess() bool {
return p.Status == PaymentStatusSuccess
}
// IsFailed 检查支付是否失败
func (p *Payment) IsFailed() bool {
return p.Status == PaymentStatusFailed
}
// IsRefunded 检查是否已退款
func (p *Payment) IsRefunded() bool {
return p.Status == PaymentStatusRefunded
}
// NewPayment 创建支付订单
func NewPayment(orderNo string, userID, targetID uint64, paymentType PaymentType, amount int64, description string) *Payment {
return &Payment{
OrderNo: orderNo,
UserID: userID,
TargetID: targetID,
Type: paymentType,
Amount: amount,
Status: PaymentStatusPending,
Description: description,
}
}

@ -0,0 +1,16 @@
package payment
import "errors"
var (
ErrInvalidAmount = errors.New("支付金额不能小于0")
ErrInvalidOrderNo = errors.New("订单编号不能为空")
ErrInvalidUserID = errors.New("用户ID不能为空")
ErrInvalidTargetID = errors.New("商品ID不能为空")
ErrInvalidTransactionID = errors.New("交易号不能为空")
ErrPaymentNotFound = errors.New("支付订单不存在")
ErrPaymentExists = errors.New("支付订单已存在")
ErrPaymentSuccess = errors.New("支付订单已支付成功")
ErrPaymentFailed = errors.New("支付订单已支付失败")
ErrPaymentRefunded = errors.New("支付订单已退款")
)

@ -5,29 +5,23 @@ import (
"xorm.io/builder"
)
// PaymentRepository 支付仓储接口
// Repository 支付订单仓储接口
type PaymentRepository interface {
// Save 保存支付订单
Save(payment *Payment) error
// FindByID 根据ID查找支付订单
FindByID(id uint64) (*Payment, error)
// FindByOrderNo 根据订单号查找支付订单
FindByOrderNo(orderNo string) (*Payment, error)
// FindByTransactionID 根据微信支付订单号查找支付订单
FindByTransactionID(transactionID string) (*Payment, error)
// FindByUserID 根据用户ID查找支付订单列表
FindByUserID(userID uint64, page *page.Page) error
// FindAll 查询支付订单列表
// Create 创建支付订单
Create(payment *Payment) error
// GetByID 根据ID获取支付订单
GetByID(id uint64) (*Payment, error)
// GetByOrderNo 根据订单号获取支付订单
GetByOrderNo(orderNo string) (*Payment, error)
// GetByTransactionID 根据交易号获取支付订单
GetByTransactionID(transactionID string) (*Payment, error)
// ListByUserID 获取用户支付订单列表
ListByUserID(userID uint64, page *page.Page, conds []builder.Cond) error
FindAll(page *page.Page, conds []builder.Cond) error
// Update 更新支付订单
Update(payment *Payment) error
// UpdateStatus 更新支付状态
UpdateStatus(id uint64, status PaymentStatus) error
// Delete 删除支付订单
Delete(id uint64) error
}

@ -12,14 +12,19 @@ const (
// Price 价格管理实体
type Price struct {
ID uint64 `json:"id" xorm:"pk autoincr 'id'"`
TargetID uint64 `json:"target_id" xorm:"not null 'target_id'"` // 目标ID文章ID或专栏ID
Type PriceType `json:"type" xorm:"not null 'type'"` // 价格类型
Amount int64 `json:"amount" xorm:"not null 'amount'"` // 价格(分)
AdminID uint64 `json:"admin_id" xorm:"not null 'admin_id'"` // 管理员ID
CreatedAt time.Time `json:"created_at" xorm:"created 'created_at'"`
UpdatedAt time.Time `json:"updated_at" xorm:"updated 'updated_at'"`
DeletedAt *time.Time `json:"deleted_at" xorm:"deleted 'deleted_at'"`
ID uint64 `xorm:"pk autoincr 'id'"`
TargetID uint64 `xorm:"not null 'target_id'"` // 目标ID文章ID或专栏ID
Type PriceType `xorm:"not null 'type'"` // 价格类型
Amount int64 `xorm:"not null 'amount'"` // 基础价格(分)
OneMonthPrice int64 `xorm:"not null 'one_month_price'"` // 1个月价格
ThreeMonthsPrice int64 `xorm:"not null 'three_months_price'"` // 3个月价格
SixMonthsPrice int64 `xorm:"not null 'six_months_price'"` // 6个月价格
OneYearPrice int64 `xorm:"not null 'one_year_price'"` // 1年价格
Discount float32 `xorm:"not null 'discount'"` // 折扣(分)
AdminID uint64 `xorm:"not null 'admin_id'"` // 管理员ID
CreatedAt time.Time `xorm:"created 'created_at'"`
UpdatedAt time.Time `xorm:"updated 'updated_at'"`
DeletedAt *time.Time `xorm:"deleted 'deleted_at'"`
}
// DefaultPrice 默认价格(分)
@ -27,3 +32,18 @@ const (
DefaultArticlePrice = 1000 // 文章默认价格10元
DefaultColumnPrice = 10000 // 专栏默认价格100元
)
// NewPrice 创建价格实体
func NewPrice(targetID uint64, priceType PriceType, amount int64, adminID uint64) *Price {
return &Price{
TargetID: targetID,
Type: priceType,
Amount: amount,
OneMonthPrice: amount,
ThreeMonthsPrice: int64(float64(amount) * 0.9), // 9折
SixMonthsPrice: int64(float64(amount) * 0.8), // 8折
OneYearPrice: int64(float64(amount) * 0.7), // 7折
AdminID: adminID,
Discount: 0.3,
}
}

@ -10,12 +10,12 @@ const (
ContentTypeColumn ContentType = 2 // 专栏
)
//ContentSource 内容来源
// ContentSource 内容来源
type ContentSource int8
const (
ContentSourceBuy ContentSource = 1 //购买
ContentSourceGift ContentSource = 1 //购买
ContentSourceBuy ContentSource = 1 // 购买
ContentSourceGift ContentSource = 2 // 赠送
)
// Purchase 内容购买记录
@ -25,6 +25,8 @@ type Purchase struct {
ContentId uint64 `xorm:"notnull 'content_id'" json:"contentId"`
ContentType ContentType `xorm:"tinyint(1) notnull 'content_type'" `
Price float64 `xorm:"decimal(10,2) notnull 'price'" json:"price"`
Duration int `xorm:"notnull 'duration'" json:"duration"` // 购买时长文章为0表示永久
ExpiredAt time.Time `xorm:"notnull 'expired_at'" json:"expiredAt"` // 过期时间文章为null表示永久
ContentSource ContentSource `xorm:"tinyint(1) notnull 'content_source'" `
Status int8 `xorm:"tinyint(1) default 1 'status'" json:"status"`
CreatedAt time.Time `xorm:"datetime created 'created_at'" json:"createdAt"`
@ -33,16 +35,30 @@ type Purchase struct {
// IsValid 检查购买记录是否有效
func (p *Purchase) IsValid() bool {
return p.Status == 1
if p.Status != 1 {
return false
}
if p.ContentType == ContentTypeArticle {
return true // 文章永久有效
}
return time.Now().Before(p.ExpiredAt)
}
// NewPurchase 创建购买记录
func NewPurchase(userId, contentId uint64, contentType ContentType, price float64) *Purchase {
return &Purchase{
func NewPurchase(userId, contentId uint64, contentType ContentType, price float64, duration int) *Purchase {
purchase := &Purchase{
UserId: userId,
ContentId: contentId,
ContentType: contentType,
Price: price,
Duration: duration,
Status: 1,
}
// 设置过期时间
if contentType == ContentTypeColumn {
purchase.ExpiredAt = time.Now().AddDate(0, duration, 0)
}
return purchase
}

@ -8,8 +8,8 @@ type Repository interface {
FindByUserIdAndContent(userId, contentId uint64, contentType ContentType) (*Purchase, error)
// FindByUserId 查找用户的所有购买记录
FindByUserId(userId uint64) ([]*Purchase, error)
FindColumnById(ids ...uint64) ([]*Purchase, error)
FindArticleById(ids ...uint64) ([]*Purchase, error)
FindColumnById(uid uint64, ids ...uint64) ([]*Purchase, error)
FindArticleById(uid uint64, ids ...uint64) ([]*Purchase, error)
FindArticlesByUserId(userId uint64) ([]*Purchase, error)
FindColumnsByUserId(userId uint64) ([]*Purchase, error)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save