feat(新功能):新增支付界面;前端新增优惠券;后端新增优惠券逻辑;完善订购详情页面;
@ -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)}}
|
||||
})
|
||||
}
|
||||
}
|
@ -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,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';
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<p>
|
||||
coupon-item works!
|
||||
</p>
|
@ -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}个月`;
|
||||
}
|
||||
}
|
@ -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,3 @@
|
||||
img{
|
||||
width: 100%;
|
||||
}
|
@ -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,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}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 953 KiB |
After Width: | Height: | Size: 602 KiB |
After Width: | Height: | Size: 759 KiB |
After Width: | Height: | Size: 838 KiB |
After Width: | Height: | Size: 1006 KiB |
After Width: | Height: | Size: 663 KiB |
After Width: | Height: | Size: 915 KiB |
After Width: | Height: | Size: 750 KiB |
After Width: | Height: | Size: 744 KiB |
After Width: | Height: | Size: 22 KiB |
@ -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,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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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,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
|
||||
}
|
@ -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("支付订单已退款")
|
||||
)
|