diff --git a/cls/src/app/app-routing.module.ts b/cls/src/app/app-routing.module.ts index 30ae12a..44f54a8 100644 --- a/cls/src/app/app-routing.module.ts +++ b/cls/src/app/app-routing.module.ts @@ -20,7 +20,7 @@ const routes: Routes = [ @NgModule({ imports: [ - RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) + RouterModule.forRoot(routes) ], exports: [RouterModule] }) diff --git a/cls/src/app/app.module.ts b/cls/src/app/app.module.ts index 1dc1acc..16fc4b3 100644 --- a/cls/src/app/app.module.ts +++ b/cls/src/app/app.module.ts @@ -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, -// }, diff --git a/cls/src/app/core/services/auth.service.ts b/cls/src/app/core/services/auth.service.ts new file mode 100644 index 0000000..f7e7b5e --- /dev/null +++ b/cls/src/app/core/services/auth.service.ts @@ -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 { + 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'); + } +} \ No newline at end of file diff --git a/cls/src/app/home/article-buy/article-buy-routing.module.ts b/cls/src/app/home/article-buy/article-buy-routing.module.ts new file mode 100644 index 0000000..165d1f5 --- /dev/null +++ b/cls/src/app/home/article-buy/article-buy-routing.module.ts @@ -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 {} diff --git a/cls/src/app/home/article-buy/article-buy.module.ts b/cls/src/app/home/article-buy/article-buy.module.ts new file mode 100644 index 0000000..b9a2e74 --- /dev/null +++ b/cls/src/app/home/article-buy/article-buy.module.ts @@ -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 {} diff --git a/cls/src/app/home/article-buy/article-buy.page.html b/cls/src/app/home/article-buy/article-buy.page.html new file mode 100644 index 0000000..b551ef6 --- /dev/null +++ b/cls/src/app/home/article-buy/article-buy.page.html @@ -0,0 +1,63 @@ + + + + + + 订购文章 + + + + + + + + +
+
+ 永久有效 +
+
+ 单篇解锁 + {{(price?.discount || 0.3) * 10}}折 +
+
+ ¥{{price?.amount || 1888}} + ¥{{getDiscountPrice(this.price.amount,this.price.discount)|| 566.4}} + +
+
+
+
+ + +
+ 更多折扣优惠 >> +
+ + + + + + + + + + + +
+ + + 确认支付 ¥{{getDiscountPriceAndCoupon(price!.amount, price!.discount)}} + + + +
+
diff --git a/cls/src/app/home/article-buy/article-buy.page.scss b/cls/src/app/home/article-buy/article-buy.page.scss new file mode 100644 index 0000000..31bb2d8 --- /dev/null +++ b/cls/src/app/home/article-buy/article-buy.page.scss @@ -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; +} + diff --git a/cls/src/app/home/article-buy/article-buy.page.ts b/cls/src/app/home/article-buy/article-buy.page.ts new file mode 100644 index 0000000..4fe2d07 --- /dev/null +++ b/cls/src/app/home/article-buy/article-buy.page.ts @@ -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)}} + }) + } +} diff --git a/cls/src/app/home/article-detail/article-detail.module.ts b/cls/src/app/home/article-detail/article-detail.module.ts index 597634c..2c87875 100644 --- a/cls/src/app/home/article-detail/article-detail.module.ts +++ b/cls/src/app/home/article-detail/article-detail.module.ts @@ -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: [ diff --git a/cls/src/app/home/article-detail/article-detail.page.ts b/cls/src/app/home/article-detail/article-detail.page.ts index 51635a5..efef111 100644 --- a/cls/src/app/home/article-detail/article-detail.page.ts +++ b/cls/src/app/home/article-detail/article-detail.page.ts @@ -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(); }); } -} \ No newline at end of file +} diff --git a/cls/src/app/home/component/image-preview/image-preview.component.html b/cls/src/app/home/article-detail/image-preview/image-preview.component.html similarity index 100% rename from cls/src/app/home/component/image-preview/image-preview.component.html rename to cls/src/app/home/article-detail/image-preview/image-preview.component.html diff --git a/cls/src/app/home/component/image-preview/image-preview.component.scss b/cls/src/app/home/article-detail/image-preview/image-preview.component.scss similarity index 100% rename from cls/src/app/home/component/image-preview/image-preview.component.scss rename to cls/src/app/home/article-detail/image-preview/image-preview.component.scss diff --git a/cls/src/app/home/article-detail/image-preview/image-preview.component.ts b/cls/src/app/home/article-detail/image-preview/image-preview.component.ts new file mode 100644 index 0000000..e86ae8e --- /dev/null +++ b/cls/src/app/home/article-detail/image-preview/image-preview.component.ts @@ -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(); + } +} diff --git a/cls/src/app/home/column-buy/column-buy-routing.module.ts b/cls/src/app/home/column-buy/column-buy-routing.module.ts new file mode 100644 index 0000000..64a0e1a --- /dev/null +++ b/cls/src/app/home/column-buy/column-buy-routing.module.ts @@ -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 {} diff --git a/cls/src/app/home/column-buy/column-buy.module.ts b/cls/src/app/home/column-buy/column-buy.module.ts new file mode 100644 index 0000000..11eefd7 --- /dev/null +++ b/cls/src/app/home/column-buy/column-buy.module.ts @@ -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 {} diff --git a/cls/src/app/home/column-buy/column-buy.page.html b/cls/src/app/home/column-buy/column-buy.page.html new file mode 100644 index 0000000..b397066 --- /dev/null +++ b/cls/src/app/home/column-buy/column-buy.page.html @@ -0,0 +1,100 @@ + + + + + + 购买专栏 + + + + + +
+ +
+ +
+
+

{{column?.title}}

+

{{column?.brief}}

+
+
+ + +
+
+ +
+
¥{{getDailyPrice(columnPrice?.oneMonthPrice || 0, 1)}}/天
+
1个月
+
+ ¥{{getDiscountPrice(columnPrice?.oneMonthPrice || 0,columnPrice.discount)}} + ¥{{columnPrice?.oneMonthPrice}} +
+
{{(columnPrice?.discount || 0.3) * 10}}折
+
+ + +
+
¥{{getDailyPrice(columnPrice?.threeMonthsPrice || 0, 3)}}/天
+
3个月
+
+ ¥{{getDiscountPrice(columnPrice?.threeMonthsPrice || 0,columnPrice.discount)}} + ¥{{columnPrice?.threeMonthsPrice}} +
+
{{(columnPrice?.discount || 0.3) * 10}}折
+
+
+ +
+ +
+
¥{{getDailyPrice(columnPrice?.sixMonthsPrice || 0, 6)}}/天
+
6个月
+
+ ¥{{getDiscountPrice(columnPrice?.sixMonthsPrice || 0,columnPrice.discount)}} + ¥{{columnPrice?.sixMonthsPrice}} +
+
{{(columnPrice?.discount || 0.3) * 10}}折
+
+ + +
+
¥{{getDailyPrice(columnPrice?.oneYearPrice || 0, 12)}}/天
+
1年
+
+ ¥{{getDiscountPrice(columnPrice?.oneYearPrice || 0,columnPrice.discount)}} + ¥{{columnPrice?.oneYearPrice}} +
+
{{(columnPrice?.discount || 0.3) * 10}}折
+
+
+
+ + +
+ 更多折扣优惠 >> +
+ + + + + + + + + + + +
+ + + 确认支付 ¥{{getDiscountPriceAndCoupon(this.getSelectedPrice()|| 0,columnPrice.discount) - couponAmount}} + + + +
+
diff --git a/cls/src/app/home/column-buy/column-buy.page.scss b/cls/src/app/home/column-buy/column-buy.page.scss new file mode 100644 index 0000000..b9ee27f --- /dev/null +++ b/cls/src/app/home/column-buy/column-buy.page.scss @@ -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; +} diff --git a/cls/src/app/home/column-buy/column-buy.page.ts b/cls/src/app/home/column-buy/column-buy.page.ts new file mode 100644 index 0000000..ff96de6 --- /dev/null +++ b/cls/src/app/home/column-buy/column-buy.page.ts @@ -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'; + } +} diff --git a/cls/src/app/home/component/article-content/article-content.component.ts b/cls/src/app/home/component/article-content/article-content.component.ts index 5c410de..6a03d7e 100644 --- a/cls/src/app/home/component/article-content/article-content.component.ts +++ b/cls/src/app/home/component/article-content/article-content.component.ts @@ -39,6 +39,8 @@ export class ArticleContentComponent implements OnInit { return this.getUnLockData() case "free": return this.getFreeData() + default: + return this.getNewData() } } diff --git a/cls/src/app/home/component/article-item/article-item.component.html b/cls/src/app/home/component/article-item/article-item.component.html index f249ca7..8f6370b 100644 --- a/cls/src/app/home/component/article-item/article-item.component.html +++ b/cls/src/app/home/component/article-item/article-item.component.html @@ -1,4 +1,4 @@ -
+
@@ -11,8 +11,8 @@ 创业板 {{article.growthBoard}}只
-
- +
+
diff --git a/cls/src/app/home/component/article-item/article-item.component.ts b/cls/src/app/home/component/article-item/article-item.component.ts index caa8c80..e8c6a4d 100644 --- a/cls/src/app/home/component/article-item/article-item.component.ts +++ b/cls/src/app/home/component/article-item/article-item.component.ts @@ -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} }); diff --git a/cls/src/app/home/component/coupon-item/coupon-item.component.html b/cls/src/app/home/component/coupon-item/coupon-item.component.html new file mode 100644 index 0000000..4dfc522 --- /dev/null +++ b/cls/src/app/home/component/coupon-item/coupon-item.component.html @@ -0,0 +1,3 @@ +

+ coupon-item works! +

diff --git a/cls/src/app/home/component/coupon-item/coupon-item.component.scss b/cls/src/app/home/component/coupon-item/coupon-item.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/cls/src/app/home/component/coupon-item/coupon-item.component.ts b/cls/src/app/home/component/coupon-item/coupon-item.component.ts new file mode 100644 index 0000000..b408f5d --- /dev/null +++ b/cls/src/app/home/component/coupon-item/coupon-item.component.ts @@ -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() {} + +} diff --git a/cls/src/app/home/component/coupon-list/coupon-list.component.html b/cls/src/app/home/component/coupon-list/coupon-list.component.html new file mode 100644 index 0000000..617f6f7 --- /dev/null +++ b/cls/src/app/home/component/coupon-list/coupon-list.component.html @@ -0,0 +1,47 @@ +
+
+

优惠券

+ {{ couponAmount!= 0 ?'-':'' }}¥ {{couponAmount}} +
+ 暂无可用优惠券 +
+
+ + + +
+ +
+ +
+
+ +
不使用优惠券
+
+ +
+ +
+
+ ¥ + {{coupon.value}} +
+
满{{coupon.minAmount}}元可用
+
+
+
{{coupon.name}}
+
有效期至 {{coupon.endTime | date:'yyyy.MM.dd'}}
+
+
+
+
+
diff --git a/cls/src/app/home/component/coupon-list/coupon-list.component.scss b/cls/src/app/home/component/coupon-list/coupon-list.component.scss new file mode 100644 index 0000000..8807611 --- /dev/null +++ b/cls/src/app/home/component/coupon-list/coupon-list.component.scss @@ -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; + } + } + } + } +} diff --git a/cls/src/app/home/component/coupon-list/coupon-list.component.ts b/cls/src/app/home/component/coupon-list/coupon-list.component.ts new file mode 100644 index 0000000..a2d24f5 --- /dev/null +++ b/cls/src/app/home/component/coupon-list/coupon-list.component.ts @@ -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(); + + 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; + } +} diff --git a/cls/src/app/home/component/image-preview/image-preview.component.ts b/cls/src/app/home/component/image-preview/image-preview.component.ts deleted file mode 100644 index 34ae52e..0000000 --- a/cls/src/app/home/component/image-preview/image-preview.component.ts +++ /dev/null @@ -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(); - } -} diff --git a/cls/src/app/home/component/more-discounts/more-discounts.component.html b/cls/src/app/home/component/more-discounts/more-discounts.component.html new file mode 100644 index 0000000..c5bd2d6 --- /dev/null +++ b/cls/src/app/home/component/more-discounts/more-discounts.component.html @@ -0,0 +1,16 @@ +
+
+

WELCOME TO CAIJIANZUA

+ +
+ +
+
+
请联系客服
+
领取现金优惠券
+
+
+ 客服二维码 +
+
+
diff --git a/cls/src/app/home/component/more-discounts/more-discounts.component.scss b/cls/src/app/home/component/more-discounts/more-discounts.component.scss new file mode 100644 index 0000000..c3d6500 --- /dev/null +++ b/cls/src/app/home/component/more-discounts/more-discounts.component.scss @@ -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; + } + } +} diff --git a/cls/src/app/home/component/more-discounts/more-discounts.component.ts b/cls/src/app/home/component/more-discounts/more-discounts.component.ts new file mode 100644 index 0000000..df7dd8d --- /dev/null +++ b/cls/src/app/home/component/more-discounts/more-discounts.component.ts @@ -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(); + } +} diff --git a/cls/src/app/home/confirm-order/confirm-order-routing.module.ts b/cls/src/app/home/confirm-order/confirm-order-routing.module.ts new file mode 100644 index 0000000..b7c8f86 --- /dev/null +++ b/cls/src/app/home/confirm-order/confirm-order-routing.module.ts @@ -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 {} diff --git a/cls/src/app/home/confirm-order/confirm-order.module.ts b/cls/src/app/home/confirm-order/confirm-order.module.ts new file mode 100644 index 0000000..b60f54e --- /dev/null +++ b/cls/src/app/home/confirm-order/confirm-order.module.ts @@ -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 {} diff --git a/cls/src/app/home/confirm-order/confirm-order.page.html b/cls/src/app/home/confirm-order/confirm-order.page.html new file mode 100644 index 0000000..d3abd96 --- /dev/null +++ b/cls/src/app/home/confirm-order/confirm-order.page.html @@ -0,0 +1,35 @@ + + + + + 取消 + + + 支付订单 + + + + + +
+
¥{{order.amount}}
+
{{order.description}}
+
+ + +
+
+ + 微信支付 + +
+
+
+ + + + diff --git a/cls/src/app/home/confirm-order/confirm-order.page.scss b/cls/src/app/home/confirm-order/confirm-order.page.scss new file mode 100644 index 0000000..fa7d886 --- /dev/null +++ b/cls/src/app/home/confirm-order/confirm-order.page.scss @@ -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; + } + } +} diff --git a/cls/src/app/home/confirm-order/confirm-order.page.ts b/cls/src/app/home/confirm-order/confirm-order.page.ts new file mode 100644 index 0000000..1d6b3f2 --- /dev/null +++ b/cls/src/app/home/confirm-order/confirm-order.page.ts @@ -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}个月`; + } +} diff --git a/cls/src/app/home/home-routing.module.ts b/cls/src/app/home/home-routing.module.ts index 1ad420a..e3d0956 100644 --- a/cls/src/app/home/home-routing.module.ts +++ b/cls/src/app/home/home-routing.module.ts @@ -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({ diff --git a/cls/src/app/home/home.module.ts b/cls/src/app/home/home.module.ts index 3dc9d99..ba0b579 100644 --- a/cls/src/app/home/home.module.ts +++ b/cls/src/app/home/home.module.ts @@ -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] }) diff --git a/cls/src/app/home/home.service.ts b/cls/src/app/home/home.service.ts index 092b03d..053a956 100644 --- a/cls/src/app/home/home.service.ts +++ b/cls/src/app/home/home.service.ts @@ -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
{ return this.http.get
(`/api/article/detail/${uid}`) } + + getArticlePrice(targetId:number):Observable{ + return this.http.get(`/api/price/article/${targetId}`) + } + getColumnPrice(targetId:number):Observable{ + return this.http.get(`/api/price/column/${targetId}`) + } + getColumnByName(title:string):Observable{ + return this.http.get(`/api/column/get`,{params:{title}}) + } + createOrder(order:Order):Observable { + return this.http.post('/api/order/create',order) + } + getCouponList():Observable> { + console.log("开始请求") + return this.http.get>('/api/coupon/get',{}) + } } diff --git a/cls/src/app/home/special-column/column-describe/column-describe-routing.module.ts b/cls/src/app/home/special-column/column-describe/column-describe-routing.module.ts new file mode 100644 index 0000000..eaafb58 --- /dev/null +++ b/cls/src/app/home/special-column/column-describe/column-describe-routing.module.ts @@ -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 {} diff --git a/cls/src/app/home/special-column/column-describe/column-describe.module.ts b/cls/src/app/home/special-column/column-describe/column-describe.module.ts new file mode 100644 index 0000000..333f963 --- /dev/null +++ b/cls/src/app/home/special-column/column-describe/column-describe.module.ts @@ -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 {} diff --git a/cls/src/app/home/special-column/column-describe/column-describe.page.html b/cls/src/app/home/special-column/column-describe/column-describe.page.html new file mode 100644 index 0000000..fde1fa5 --- /dev/null +++ b/cls/src/app/home/special-column/column-describe/column-describe.page.html @@ -0,0 +1,12 @@ + + + + + + {{ column.title }}简介 + + + + + + diff --git a/cls/src/app/home/special-column/column-describe/column-describe.page.scss b/cls/src/app/home/special-column/column-describe/column-describe.page.scss new file mode 100644 index 0000000..5b6f3d2 --- /dev/null +++ b/cls/src/app/home/special-column/column-describe/column-describe.page.scss @@ -0,0 +1,3 @@ +img{ + width: 100%; +} diff --git a/cls/src/app/home/special-column/column-describe/column-describe.page.ts b/cls/src/app/home/special-column/column-describe/column-describe.page.ts new file mode 100644 index 0000000..93aa5d1 --- /dev/null +++ b/cls/src/app/home/special-column/column-describe/column-describe.page.ts @@ -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'; + } +} diff --git a/cls/src/app/home/special-column/special-column-routing.module.ts b/cls/src/app/home/special-column/special-column-routing.module.ts index 05a8455..4c31280 100644 --- a/cls/src/app/home/special-column/special-column-routing.module.ts +++ b/cls/src/app/home/special-column/special-column-routing.module.ts @@ -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 {} diff --git a/cls/src/app/home/special-column/special-column.page.html b/cls/src/app/home/special-column/special-column.page.html index 2ddba0c..45af647 100644 --- a/cls/src/app/home/special-column/special-column.page.html +++ b/cls/src/app/home/special-column/special-column.page.html @@ -1,5 +1,8 @@ + + + {{name}} @@ -16,7 +19,7 @@

{{name}}

-
简介
+
简介 >
@@ -37,3 +40,12 @@ + + +
+
+

盘中宝

+

重磅信息挖掘

+
+ +
diff --git a/cls/src/app/home/special-column/special-column.page.scss b/cls/src/app/home/special-column/special-column.page.scss index 154d9e8..2b2d9f4 100644 --- a/cls/src/app/home/special-column/special-column.page.scss +++ b/cls/src/app/home/special-column/special-column.page.scss @@ -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; +} diff --git a/cls/src/app/home/special-column/special-column.page.ts b/cls/src/app/home/special-column/special-column.page.ts index fc64f40..0395bcb 100644 --- a/cls/src/app/home/special-column/special-column.page.ts +++ b/cls/src/app/home/special-column/special-column.page.ts @@ -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} + }); + } } diff --git a/cls/src/app/mine/login/login.page.html b/cls/src/app/mine/login/login.page.html index c9d6168..e95fa1c 100644 --- a/cls/src/app/mine/login/login.page.html +++ b/cls/src/app/mine/login/login.page.html @@ -7,7 +7,7 @@ diff --git a/cls/src/app/mine/login/login.page.ts b/cls/src/app/mine/login/login.page.ts index 43205ea..ecb57b3 100644 --- a/cls/src/app/mine/login/login.page.ts +++ b/cls/src/app/mine/login/login.page.ts @@ -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); diff --git a/cls/src/app/mine/mine.page.ts b/cls/src/app/mine/mine.page.ts index 17f6fe8..d319fc2 100644 --- a/cls/src/app/mine/mine.page.ts +++ b/cls/src/app/mine/mine.page.ts @@ -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"; diff --git a/cls/src/app/mine/mine.service.ts b/cls/src/app/mine/mine.service.ts index 83bfd55..0a36eaf 100644 --- a/cls/src/app/mine/mine.service.ts +++ b/cls/src/app/mine/mine.service.ts @@ -20,7 +20,7 @@ export interface LoginResponse { providedIn: 'root' }) export class MineService { - private apiUrl = environment.apiUrl; + constructor(private http: HttpClient, private modalController: ModalController) {} diff --git a/cls/src/app/services/payment.service.ts b/cls/src/app/services/payment.service.ts new file mode 100644 index 0000000..1ffb32a --- /dev/null +++ b/cls/src/app/services/payment.service.ts @@ -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 { + return this.http.post(this.apiUrl, request); + } + + getPayment(orderNo: string): Observable { + return this.http.get(`${this.apiUrl}/${orderNo}`); + } + + getPaymentStatus(orderNo: string): Observable<{ status: string }> { + return this.http.get<{ status: string }>(`${this.apiUrl}/${orderNo}/status`); + } + + getWechatPayParams(orderNo: string): Observable { + return this.http.get(`${this.apiUrl}/${orderNo}/wechat-pay`); + } + + // 唤起微信支付 + callWechatPay(orderNo: string): Observable { + return this.getWechatPayParams(orderNo).pipe( + map(params => { + if (typeof WeixinJSBridge === 'undefined') { + throw new Error('请在微信浏览器中打开'); + } + + return new Promise((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) + }) + ); + } +} diff --git a/cls/src/app/shared/model/article.ts b/cls/src/app/shared/model/article.ts index 495d5fe..7e613b2 100644 --- a/cls/src/app/shared/model/article.ts +++ b/cls/src/app/shared/model/article.ts @@ -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; } diff --git a/cls/src/app/shared/model/column.ts b/cls/src/app/shared/model/column.ts new file mode 100644 index 0000000..4a88458 --- /dev/null +++ b/cls/src/app/shared/model/column.ts @@ -0,0 +1,10 @@ +export interface Column { + id:number; + title:string; + brief:string; + cover:string; + articleNum:number; + followNum:number; + purchaseNum:number; + unlock:boolean; +} diff --git a/cls/src/app/shared/model/coupon.ts b/cls/src/app/shared/model/coupon.ts new file mode 100644 index 0000000..121a988 --- /dev/null +++ b/cls/src/app/shared/model/coupon.ts @@ -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', +} diff --git a/cls/src/app/shared/model/order.ts b/cls/src/app/shared/model/order.ts new file mode 100644 index 0000000..61df980 --- /dev/null +++ b/cls/src/app/shared/model/order.ts @@ -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; +} diff --git a/cls/src/app/shared/model/price.ts b/cls/src/app/shared/model/price.ts new file mode 100644 index 0000000..88144cc --- /dev/null +++ b/cls/src/app/shared/model/price.ts @@ -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; +} diff --git a/cls/src/assets/class_des/clzzd.png b/cls/src/assets/class_des/clzzd.png new file mode 100644 index 0000000..012e942 Binary files /dev/null and b/cls/src/assets/class_des/clzzd.png differ diff --git a/cls/src/assets/class_des/dbjd.png b/cls/src/assets/class_des/dbjd.png new file mode 100644 index 0000000..e782e91 Binary files /dev/null and b/cls/src/assets/class_des/dbjd.png differ diff --git a/cls/src/assets/class_des/fkyb.png b/cls/src/assets/class_des/fkyb.png new file mode 100644 index 0000000..8aa9fb3 Binary files /dev/null and b/cls/src/assets/class_des/fkyb.png differ diff --git a/cls/src/assets/class_des/ggqzd.png b/cls/src/assets/class_des/ggqzd.png new file mode 100644 index 0000000..820f1dc Binary files /dev/null and b/cls/src/assets/class_des/ggqzd.png differ diff --git a/cls/src/assets/class_des/jdtg.png b/cls/src/assets/class_des/jdtg.png new file mode 100644 index 0000000..abf1508 Binary files /dev/null and b/cls/src/assets/class_des/jdtg.png differ diff --git a/cls/src/assets/class_des/jpjyk.png b/cls/src/assets/class_des/jpjyk.png new file mode 100644 index 0000000..5397b0d Binary files /dev/null and b/cls/src/assets/class_des/jpjyk.png differ diff --git a/cls/src/assets/class_des/lhb.png b/cls/src/assets/class_des/lhb.png new file mode 100644 index 0000000..4a32d5d Binary files /dev/null and b/cls/src/assets/class_des/lhb.png differ diff --git a/cls/src/assets/class_des/pzb.png b/cls/src/assets/class_des/pzb.png new file mode 100644 index 0000000..4c2b8ee Binary files /dev/null and b/cls/src/assets/class_des/pzb.png differ diff --git a/cls/src/assets/class_des/yx.png b/cls/src/assets/class_des/yx.png new file mode 100644 index 0000000..2564746 Binary files /dev/null and b/cls/src/assets/class_des/yx.png differ diff --git a/cls/src/assets/order/avatar.png b/cls/src/assets/order/avatar.png new file mode 100644 index 0000000..e6d6f45 Binary files /dev/null and b/cls/src/assets/order/avatar.png differ diff --git a/cls/src/global.scss b/cls/src/global.scss index d729947..012bc46 100644 --- a/cls/src/global.scss +++ b/cls/src/global.scss @@ -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; + } +} diff --git a/go.mod b/go.mod index 5bbd7aa..32fbe62 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 3057d61..b885a56 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/application/article/service.go b/internal/application/article/service.go index 625ddc9..b788c38 100644 --- a/internal/application/article/service.go +++ b/internal/application/article/service.go @@ -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 } diff --git a/internal/application/column/assembler.go b/internal/application/column/assembler.go new file mode 100644 index 0000000..d5daac0 --- /dev/null +++ b/internal/application/column/assembler.go @@ -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, + } +} diff --git a/internal/application/column/dto.go b/internal/application/column/dto.go new file mode 100644 index 0000000..08b613b --- /dev/null +++ b/internal/application/column/dto.go @@ -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"` // 每页数量 +} diff --git a/internal/application/column/service.go b/internal/application/column/service.go new file mode 100644 index 0000000..471b910 --- /dev/null +++ b/internal/application/column/service.go @@ -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 +} diff --git a/internal/application/coupon/dto.go b/internal/application/coupon/dto.go new file mode 100644 index 0000000..f7c2b60 --- /dev/null +++ b/internal/application/coupon/dto.go @@ -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"` // 优惠券状态 +} diff --git a/internal/application/coupon/service.go b/internal/application/coupon/service.go new file mode 100644 index 0000000..9218b75 --- /dev/null +++ b/internal/application/coupon/service.go @@ -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 +} diff --git a/internal/application/order/dto.go b/internal/application/order/dto.go new file mode 100644 index 0000000..47d52ca --- /dev/null +++ b/internal/application/order/dto.go @@ -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"` // 更新时间 +} diff --git a/internal/application/order/service.go b/internal/application/order/service.go new file mode 100644 index 0000000..807a79a --- /dev/null +++ b/internal/application/order/service.go @@ -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) +} diff --git a/internal/application/payment/dto.go b/internal/application/payment/dto.go new file mode 100644 index 0000000..f1f4e20 --- /dev/null +++ b/internal/application/payment/dto.go @@ -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, + } +} diff --git a/internal/application/payment/service.go b/internal/application/payment/service.go index 6bf7a2b..16d28be 100644 --- a/internal/application/payment/service.go +++ b/internal/application/payment/service.go @@ -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 创建支付订单 diff --git a/internal/application/payment/service_impl.go b/internal/application/payment/service_impl.go deleted file mode 100644 index 58e793b..0000000 --- a/internal/application/payment/service_impl.go +++ /dev/null @@ -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) -} diff --git a/internal/application/price/dto.go b/internal/application/price/dto.go new file mode 100644 index 0000000..e0d25fe --- /dev/null +++ b/internal/application/price/dto.go @@ -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 +} diff --git a/internal/application/price/service.go b/internal/application/price/service.go index 0032a84..5b7efd9 100644 --- a/internal/application/price/service.go +++ b/internal/application/price/service.go @@ -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) +} diff --git a/internal/application/purchase/service.go b/internal/application/purchase/service.go index d75bc51..783c9dc 100644 --- a/internal/application/purchase/service.go +++ b/internal/application/purchase/service.go @@ -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) } diff --git a/internal/domain/article/entity.go b/internal/domain/article/entity.go index 857e95e..3f9519d 100644 --- a/internal/domain/article/entity.go +++ b/internal/domain/article/entity.go @@ -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 } diff --git a/internal/domain/column/entity.go b/internal/domain/column/entity.go new file mode 100644 index 0000000..ef9955c --- /dev/null +++ b/internal/domain/column/entity.go @@ -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-- + } +} diff --git a/internal/domain/column/repository.go b/internal/domain/column/repository.go new file mode 100644 index 0000000..90fdd5a --- /dev/null +++ b/internal/domain/column/repository.go @@ -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 +} diff --git a/internal/domain/coupon/entity.go b/internal/domain/coupon/entity.go new file mode 100644 index 0000000..ae917fc --- /dev/null +++ b/internal/domain/coupon/entity.go @@ -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 +} diff --git a/internal/domain/coupon/repository.go b/internal/domain/coupon/repository.go new file mode 100644 index 0000000..c284cf6 --- /dev/null +++ b/internal/domain/coupon/repository.go @@ -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 +} diff --git a/internal/domain/order/aggregate.go b/internal/domain/order/aggregate.go new file mode 100644 index 0000000..6b21370 --- /dev/null +++ b/internal/domain/order/aggregate.go @@ -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() +} diff --git a/internal/domain/order/aggregate_repository.go b/internal/domain/order/aggregate_repository.go new file mode 100644 index 0000000..02c50a0 --- /dev/null +++ b/internal/domain/order/aggregate_repository.go @@ -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 +} diff --git a/internal/domain/order/entity.go b/internal/domain/order/entity.go new file mode 100644 index 0000000..44e5db0 --- /dev/null +++ b/internal/domain/order/entity.go @@ -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 +} diff --git a/internal/domain/order/repository.go b/internal/domain/order/repository.go new file mode 100644 index 0000000..8348ff1 --- /dev/null +++ b/internal/domain/order/repository.go @@ -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 +} diff --git a/internal/domain/payment/entity.go b/internal/domain/payment/entity.go index 9d32aea..acce724 100644 --- a/internal/domain/payment/entity.go +++ b/internal/domain/payment/entity.go @@ -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, + } } diff --git a/internal/domain/payment/errors.go b/internal/domain/payment/errors.go new file mode 100644 index 0000000..eaafcac --- /dev/null +++ b/internal/domain/payment/errors.go @@ -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("支付订单已退款") +) diff --git a/internal/domain/payment/repository.go b/internal/domain/payment/repository.go index e79f282..0c2d3d5 100644 --- a/internal/domain/payment/repository.go +++ b/internal/domain/payment/repository.go @@ -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 } diff --git a/internal/domain/price/entity.go b/internal/domain/price/entity.go index a2cf146..b46409b 100644 --- a/internal/domain/price/entity.go +++ b/internal/domain/price/entity.go @@ -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, + } +} diff --git a/internal/domain/purchase/entity.go b/internal/domain/purchase/entity.go index 916956e..0cabeb1 100644 --- a/internal/domain/purchase/entity.go +++ b/internal/domain/purchase/entity.go @@ -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 } diff --git a/internal/domain/purchase/repository.go b/internal/domain/purchase/repository.go index de5293d..8ee1bf8 100644 --- a/internal/domain/purchase/repository.go +++ b/internal/domain/purchase/repository.go @@ -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) diff --git a/internal/domain/user/aggregate.go b/internal/domain/user/aggregate.go new file mode 100644 index 0000000..b8d0021 --- /dev/null +++ b/internal/domain/user/aggregate.go @@ -0,0 +1,74 @@ +package user + +import ( + "cls/internal/domain/purchase" + "errors" +) + +// UserAggregate 用户聚合根 +type UserAggregate struct { + user *User + purchases []*purchase.Purchase +} + +// GetUser 获取用户实体 +func (a *UserAggregate) GetUser() *User { + return a.user +} + +// GetPurchases 获取购买记录列表 +func (a *UserAggregate) GetPurchases() []*purchase.Purchase { + return a.purchases +} + +// SetPurchases 设置购买记录列表 +func (a *UserAggregate) SetPurchases(purchases []*purchase.Purchase) { + a.purchases = purchases +} + +// NewUserAggregate 创建用户聚合根 +func NewUserAggregate(user *User) *UserAggregate { + return &UserAggregate{ + user: user, + purchases: make([]*purchase.Purchase, 0), + } +} + +// UnlockArticle 解锁文章 +func (a *UserAggregate) UnlockArticle(articleId uint64) error { + // 1. 验证赠送次数 + if a.user.GiftCount <= 0 { + return errors.New("赠送次数不足") + } + + // 2. 检查是否已解锁 + if a.HasUnlocked(articleId) { + return errors.New("文章已解锁") + } + + // 3. 创建购买记录 + newPurchase := purchase.NewPurchase( + a.user.Id, + articleId, + purchase.ContentTypeArticle, + 0, // 赠送价格为0, + 0, //表示永久 + ) + newPurchase.ContentSource = purchase.ContentSourceGift + + // 4. 更新状态 + a.purchases = append(a.purchases, newPurchase) + a.user.GiftCount-- + + return nil +} + +// HasUnlocked 检查是否已解锁 +func (a *UserAggregate) HasUnlocked(articleId uint64) bool { + for _, p := range a.purchases { + if p.ContentId == articleId { + return true + } + } + return false +} diff --git a/internal/domain/user/entity.go b/internal/domain/user/entity.go index 932219b..6eff274 100644 --- a/internal/domain/user/entity.go +++ b/internal/domain/user/entity.go @@ -40,58 +40,3 @@ type User struct { UpdatedAt time.Time `xorm:"datetime updated 'updated_at'"` DeletedAt time.Time `xorm:"datetime deleted 'deleted_at'" ` } - -type userConfiguration func(u *User) -type userConfigurations []userConfiguration - -// WithUsername 设置用户名 -func WithUsername(username string) userConfiguration { - return func(u *User) { - u.Username = username - } -} - -// WithPhone 设置手机号 -func WithPhone(phone string) userConfiguration { - return func(u *User) { - u.Phone = phone - } -} - -// NewUser 创建新用户(聚合根的工厂方法) -func NewUser(username, phone string, configs ...userConfiguration) *User { - user := &User{ - Username: username, - Phone: phone, - Status: 1, - } - - userConfigurations(configs).apply(user) - return user -} - -func (cfg userConfigurations) apply(user *User) { - for _, c := range cfg { - c(user) - } -} - -// GetUsername 实现UserDetails接口 -func (u *User) GetUsername() string { - return u.Username -} - -// GetPassword 获取加密后的密码(实现UserDetails接口) -func (u *User) GetPassword() string { - return u.Password -} - -// IsEnabled 判断用户是否启用 -func (u *User) IsEnabled() bool { - return u.Status == 1 -} - -// GetPhone 获取手机号 -func (u *User) GetPhone() string { - return u.Phone -} diff --git a/internal/domain/user/repository.go b/internal/domain/user/repository.go index dee0fc4..9de983f 100644 --- a/internal/domain/user/repository.go +++ b/internal/domain/user/repository.go @@ -40,3 +40,12 @@ type UserRepository interface { //锁定状态 IsLocked(status int8) bool } + +// UserAggregateRepository 用户聚合根仓储接口 +type UserAggregateRepository interface { + // GetUserAggregate 获取用户聚合根 + GetUserAggregate(phone string) (*UserAggregate, error) + + // SaveUserAggregate 保存用户聚合根 + SaveUserAggregate(aggregate *UserAggregate) error +} diff --git a/internal/infrastructure/middleware/auth/config.go b/internal/infrastructure/middleware/auth/config.go index 3c00988..d6dc871 100644 --- a/internal/infrastructure/middleware/auth/config.go +++ b/internal/infrastructure/middleware/auth/config.go @@ -17,6 +17,7 @@ func DefaultConfig() *Config { "/api/auth/captcha/sms", "/api/auth/verify-image", "/api/auth/verify-sms", + "/api/article/all", }, TokenKey: "Authorization", } diff --git a/internal/infrastructure/persistence/column/column_repo.go b/internal/infrastructure/persistence/column/column_repo.go new file mode 100644 index 0000000..a321ff1 --- /dev/null +++ b/internal/infrastructure/persistence/column/column_repo.go @@ -0,0 +1,77 @@ +package column + +import ( + "cls/internal/domain/column" + "cls/pkg/util/page" + "cls/pkg/xorm_engine" + "errors" + "xorm.io/builder" +) + +// ColumnRepository 专栏仓储实现 +type ColumnRepositoryORM struct { + engine *xorm_engine.Engine +} + +// NewColumnRepository 创建专栏仓储 +func NewColumnRepositoryORM(engine *xorm_engine.Engine) column.ColumnRepository { + return &ColumnRepositoryORM{engine} +} + +var _ column.ColumnRepository = (*ColumnRepositoryORM)(nil) + +// Save 保存专栏 +func (r *ColumnRepositoryORM) Save(col *column.Column) error { + _, err := r.engine.Insert(col) + return err +} + +// FindByID 根据ID查找专栏 +func (r *ColumnRepositoryORM) FindByID(id uint64) (*column.Column, error) { + var col column.Column + exists, err := r.engine.ID(id).Get(&col) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + return &col, nil +} + +// FindByAuthorID 查找作者的所有专栏 +func (r *ColumnRepositoryORM) FindByAuthorID(authorID uint64) ([]*column.Column, error) { + var cols []*column.Column + err := r.engine.Where("author_id = ?", authorID). + Desc("created_at"). + Find(&cols) + return cols, err +} + +// FindAll 查询专栏列表 +func (r *ColumnRepositoryORM) FindAll(page *page.Page, conds []builder.Cond) error { + return r.engine.FindAll(page, &column.Column{}, builder.And(conds...)) +} + +// Update 更新专栏 +func (r *ColumnRepositoryORM) Update(col *column.Column) error { + _, err := r.engine.ID(col.ID).Update(col) + return err +} + +// Delete 删除专栏 +func (r *ColumnRepositoryORM) Delete(id uint64) error { + _, err := r.engine.ID(id).Delete(&column.Column{}) + return err +} +func (r *ColumnRepositoryORM) FindByName(name string) (*column.Column, error) { + var col column.Column + exists, err := r.engine.Where(builder.Eq{"title": name}).Get(&col) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.New("未找到用户") + } + return &col, nil +} diff --git a/internal/infrastructure/persistence/coupon/coupon_repo.go b/internal/infrastructure/persistence/coupon/coupon_repo.go new file mode 100644 index 0000000..b542995 --- /dev/null +++ b/internal/infrastructure/persistence/coupon/coupon_repo.go @@ -0,0 +1,83 @@ +package coupon + +import ( + "cls/internal/domain/coupon" + "cls/pkg/xorm_engine" + "xorm.io/builder" +) + +// CouponRepository 优惠券仓储实现 +type CouponRepositoryORM struct { + engine *xorm_engine.Engine +} + +var _ coupon.CouponRepository = (*CouponRepositoryORM)(nil) + +// NewCouponRepository 创建优惠券仓储 +func NewCouponRepository(engine *xorm_engine.Engine) coupon.CouponRepository { + return &CouponRepositoryORM{ + engine: engine, + } +} + +// Create 创建优惠券 +func (r *CouponRepositoryORM) Create(coupon *coupon.Coupon) error { + _, err := r.engine.Insert(coupon) + return err +} + +// GetByID 根据ID获取优惠券 +func (r *CouponRepositoryORM) GetByID(id uint64) (*coupon.Coupon, error) { + var coupon coupon.Coupon + has, err := r.engine.ID(id).Get(&coupon) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return &coupon, nil +} + +// GetByCode 根据优惠券码获取优惠券 +func (r *CouponRepositoryORM) GetByCode(code string) (*coupon.Coupon, error) { + var coupon coupon.Coupon + has, err := r.engine.Where("code = ?", code).Get(&coupon) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return &coupon, nil +} + +// List 获取优惠券列表 +func (r *CouponRepositoryORM) List(page, size int) ([]*coupon.Coupon, int64, error) { + var coupons []*coupon.Coupon + total, err := r.engine.Count(&coupon.Coupon{}) + if err != nil { + return nil, 0, err + } + err = r.engine.Limit(size, (page-1)*size).Find(&coupons) + if err != nil { + return nil, 0, err + } + return coupons, total, nil +} + +// Update 更新优惠券 +func (r *CouponRepositoryORM) Update(coupon *coupon.Coupon) error { + _, err := r.engine.ID(coupon.ID).Update(coupon) + return err +} + +// Delete 删除优惠券 +func (r *CouponRepositoryORM) Delete(id uint64) error { + _, err := r.engine.ID(id).Delete(&coupon.Coupon{}) + return err +} +func (r *CouponRepositoryORM) ListByUserID(userID uint64) ([]*coupon.Coupon, error) { + data := make([]*coupon.Coupon, 0) + return data, r.engine.Where(builder.Eq{"user_id": userID}).Find(&data) +} diff --git a/internal/infrastructure/persistence/order/aggregate_repo.go b/internal/infrastructure/persistence/order/aggregate_repo.go new file mode 100644 index 0000000..ed70fe1 --- /dev/null +++ b/internal/infrastructure/persistence/order/aggregate_repo.go @@ -0,0 +1,250 @@ +package order + +import ( + "cls/internal/domain/order" + "cls/internal/domain/payment" + "cls/internal/domain/purchase" + "cls/pkg/logger" + "cls/pkg/util/page" + "cls/pkg/xorm_engine" + "errors" + "xorm.io/builder" +) + +// AggregateRepositoryORM 订单聚合根仓储实现 +type AggregateRepositoryORM struct { + engine *xorm_engine.Engine + log logger.Logger +} + +var _ order.AggregateRepository = (*AggregateRepositoryORM)(nil) + +// NewAggregateRepositoryORM 创建订单聚合根仓储实现 +func NewAggregateRepositoryORM(engine *xorm_engine.Engine, log logger.New) order.AggregateRepository { + return &AggregateRepositoryORM{ + engine: engine, + log: log("cls:repo:order:aggregate"), + } +} + +// Save 保存订单聚合根 +func (r *AggregateRepositoryORM) Save(aggregate *order.OrderAggregate) error { + session := r.engine.NewSession() + defer session.Close() + + if err := session.Begin(); err != nil { + r.log.Error(err) + return err + } + + // 保存订单 + if _, err := session.Insert(aggregate.Order); err != nil { + r.log.Error(err) + return err + } + + // 保存支付订单 + if aggregate.Payment != nil { + if _, err := session.Insert(aggregate.Payment); err != nil { + r.log.Error(err) + return err + } + } + + // 保存购买记录 + if aggregate.Purchase != nil { + if _, err := session.Insert(aggregate.Purchase); err != nil { + session.Rollback() + r.log.Error(err) + return err + } + } + + if err := session.Commit(); err != nil { + r.log.Error(err) + return err + } + + return nil +} + +// GetByOrderNo 根据订单号获取订单聚合根 +func (r *AggregateRepositoryORM) GetByOrderNo(orderNo string) (*order.OrderAggregate, error) { + // 获取订单 + o := &order.Order{} + has, err := r.engine.Where(builder.Eq{"order_no": orderNo}).Get(o) + if err != nil { + r.log.Error(err) + return nil, err + } + if !has { + return nil, errors.New("订单不存在") + } + + // 获取支付订单 + p := &payment.Payment{} + has, err = r.engine.Where(builder.Eq{"order_no": orderNo}).Get(p) + if err != nil { + r.log.Error(err) + return nil, err + } + + // 获取购买记录 + pu := &purchase.Purchase{} + has, err = r.engine.Where(builder.Eq{"user_id": o.UserID, "content_id": o.TargetID}).Get(pu) + if err != nil { + r.log.Error(err) + return nil, err + } + + aggregate := order.NewOrderAggregate(o) + if has { + aggregate.Payment = p + aggregate.Purchase = pu + } + + return aggregate, nil +} + +// GetByID 根据ID获取订单聚合根 +func (r *AggregateRepositoryORM) GetByID(id uint64) (*order.OrderAggregate, error) { + // 获取订单 + o := &order.Order{} + has, err := r.engine.ID(id).Get(o) + if err != nil { + r.log.Error(err) + return nil, err + } + if !has { + return nil, errors.New("订单不存在") + } + + // 获取支付订单 + p := &payment.Payment{} + has, err = r.engine.Where(builder.Eq{"order_no": o.OrderNo}).Get(p) + if err != nil { + r.log.Error(err) + return nil, err + } + + // 获取购买记录 + pu := &purchase.Purchase{} + has, err = r.engine.Where(builder.Eq{"user_id": o.UserID, "content_id": o.TargetID}).Get(pu) + if err != nil { + r.log.Error(err) + return nil, err + } + + aggregate := order.NewOrderAggregate(o) + if has { + aggregate.Payment = p + aggregate.Purchase = pu + } + + return aggregate, nil +} + +// ListByUserID 获取用户订单聚合根列表 +func (r *AggregateRepositoryORM) ListByUserID(userID uint64, page *page.Page) ([]*order.OrderAggregate, int64, error) { + // 获取订单总数 + o := &order.Order{} + total, err := r.engine.Where(builder.Eq{"user_id": userID}).Count(o) + if err != nil { + r.log.Error(err) + return nil, 0, err + } + + // 获取订单列表 + var orders []*order.Order + err = r.engine.Where(builder.Eq{"user_id": userID}). + Limit(page.PageSize, (page.PageNumber-1)*page.PageSize). + Find(&orders) + if err != nil { + r.log.Error(err) + return nil, 0, err + } + + // 获取支付订单和购买记录 + aggregates := make([]*order.OrderAggregate, len(orders)) + for i, o := range orders { + aggregate := order.NewOrderAggregate(o) + + // 获取支付订单 + p := &payment.Payment{} + has, err := r.engine.Where(builder.Eq{"order_no": o.OrderNo}).Get(p) + if err != nil { + r.log.Error(err) + return nil, 0, err + } + if has { + aggregate.Payment = p + } + + // 获取购买记录 + pu := &purchase.Purchase{} + has, err = r.engine.Where(builder.Eq{"user_id": o.UserID, "content_id": o.TargetID}).Get(pu) + if err != nil { + r.log.Error(err) + return nil, 0, err + } + if has { + aggregate.Purchase = pu + } + + aggregates[i] = aggregate + } + + return aggregates, total, nil +} + +// Delete 删除订单聚合根 +func (r *AggregateRepositoryORM) Delete(id uint64) error { + session := r.engine.NewSession() + defer session.Close() + + if err := session.Begin(); err != nil { + r.log.Error(err) + return err + } + + // 获取订单 + o := &order.Order{} + has, err := session.ID(id).Get(o) + if err != nil { + session.Rollback() + r.log.Error(err) + return err + } + if !has { + session.Rollback() + return errors.New("订单不存在") + } + + // 删除支付订单 + if _, err := session.Where(builder.Eq{"order_no": o.OrderNo}).Delete(&payment.Payment{}); err != nil { + session.Rollback() + r.log.Error(err) + return err + } + + // 删除购买记录 + if _, err := session.Where(builder.Eq{"user_id": o.UserID, "content_id": o.TargetID}).Delete(&purchase.Purchase{}); err != nil { + session.Rollback() + r.log.Error(err) + return err + } + + // 删除订单 + if _, err := session.ID(id).Delete(&order.Order{}); err != nil { + session.Rollback() + r.log.Error(err) + return err + } + + if err := session.Commit(); err != nil { + r.log.Error(err) + return err + } + + return nil +} diff --git a/internal/infrastructure/persistence/order/order_repo.go b/internal/infrastructure/persistence/order/order_repo.go new file mode 100644 index 0000000..4059e32 --- /dev/null +++ b/internal/infrastructure/persistence/order/order_repo.go @@ -0,0 +1,109 @@ +package order + +import ( + "cls/internal/domain/order" + "cls/pkg/logger" + "cls/pkg/xorm_engine" + "errors" + "xorm.io/builder" +) + +// OrderRepositoryORM 订单仓储实现 +type OrderRepositoryORM struct { + engine *xorm_engine.Engine + log logger.Logger +} + +func (r *OrderRepositoryORM) Create(order *order.Order) error { + //TODO implement me + panic("implement me") +} + +func (r *OrderRepositoryORM) GetByID(id uint64) (*order.Order, error) { + //TODO implement me + panic("implement me") +} + +func (r *OrderRepositoryORM) GetByOrderNo(orderNo string) (*order.Order, error) { + //TODO implement me + panic("implement me") +} + +func (r *OrderRepositoryORM) ListByUserID(userID uint64, page, size int) ([]*order.Order, int64, error) { + //TODO implement me + panic("implement me") +} + +var _ order.OrderRepository = (*OrderRepositoryORM)(nil) + +// NewOrderRepositoryORM 创建订单仓储实现 +func NewOrderRepositoryORM(engine *xorm_engine.Engine, log logger.New) order.OrderRepository { + return &OrderRepositoryORM{ + engine: engine, + log: log("cls:repo:order"), + } +} + +// Save 保存订单 +func (r *OrderRepositoryORM) Save(order *order.Order) error { + if _, err := r.engine.Insert(order); err != nil { + r.log.Error(err) + return err + } + return nil +} + +// FindByID 根据ID获取订单 +func (r *OrderRepositoryORM) FindByID(id uint64) (*order.Order, error) { + order := &order.Order{} + has, err := r.engine.ID(id).Get(order) + if err != nil { + r.log.Error(err) + return nil, err + } + if !has { + return nil, errors.New("订单不存在") + } + return order, nil +} + +// FindByOrderNo 根据订单号获取订单 +func (r *OrderRepositoryORM) FindByOrderNo(orderNo string) (*order.Order, error) { + order := &order.Order{} + has, err := r.engine.Where(builder.Eq{"order_no": orderNo}).Get(order) + if err != nil { + r.log.Error(err) + return nil, err + } + if !has { + return nil, errors.New("订单不存在") + } + return order, nil +} + +// Update 更新订单 +func (r *OrderRepositoryORM) Update(order *order.Order) error { + if _, err := r.engine.ID(order.ID).Update(order); err != nil { + r.log.Error(err) + return err + } + return nil +} + +// UpdateStatus 更新订单状态 +func (r *OrderRepositoryORM) UpdateStatus(id uint64, status order.OrderStatus) error { + if _, err := r.engine.ID(id).Update(&order.Order{Status: status}); err != nil { + r.log.Error(err) + return err + } + return nil +} + +// Delete 删除订单 +func (r *OrderRepositoryORM) Delete(id uint64) error { + if _, err := r.engine.ID(id).Delete(&order.Order{}); err != nil { + r.log.Error(err) + return err + } + return nil +} diff --git a/internal/infrastructure/persistence/payment/payment_repo.go b/internal/infrastructure/persistence/payment/payment_repo.go index fad9411..f1ea566 100644 --- a/internal/infrastructure/persistence/payment/payment_repo.go +++ b/internal/infrastructure/persistence/payment/payment_repo.go @@ -1,4 +1,4 @@ -package payment +package repo import ( "cls/internal/domain/payment" @@ -9,101 +9,126 @@ import ( "xorm.io/builder" ) -// PaymentRepositoryORM 支付仓储实现 +// PaymentRepositoryORM 支付订单仓储实现 type PaymentRepositoryORM struct { engine *xorm_engine.Engine log logger.Logger } +func (r *PaymentRepositoryORM) Create(payment *payment.Payment) error { + //TODO implement me + panic("implement me") +} + +func (r *PaymentRepositoryORM) GetByID(id uint64) (*payment.Payment, error) { + //TODO implement me + panic("implement me") +} + +func (r *PaymentRepositoryORM) GetByOrderNo(orderNo string) (*payment.Payment, error) { + //TODO implement me + panic("implement me") +} + +func (r *PaymentRepositoryORM) GetByTransactionID(transactionID string) (*payment.Payment, error) { + //TODO implement me + panic("implement me") +} + +func (r *PaymentRepositoryORM) ListByUserID(userID uint64, page *page.Page, conds []builder.Cond) error { + //TODO implement me + panic("implement me") +} + +func (r *PaymentRepositoryORM) FindAll(page *page.Page, conds []builder.Cond) error { + //TODO implement me + panic("implement me") +} + var _ payment.PaymentRepository = (*PaymentRepositoryORM)(nil) -// NewRepository 创建支付仓储 -func NewRepository(engine *xorm_engine.Engine, log logger.New) *PaymentRepositoryORM { +// NewPaymentRepositoryORM 创建支付订单仓储实现 +func NewPaymentRepositoryORM(engine *xorm_engine.Engine, log logger.New) *PaymentRepositoryORM { return &PaymentRepositoryORM{ engine: engine, - log: log("cls:repository:payment"), + log: log("cls:repo:payment"), } } // Save 保存支付订单 -func (p *PaymentRepositoryORM) Save(payment *payment.Payment) error { - _, err := p.engine.Insert(payment) - if err != nil { - p.log.Error(err) +func (r *PaymentRepositoryORM) Save(payment *payment.Payment) error { + if _, err := r.engine.Insert(payment); err != nil { + r.log.Error(err) + return err } - return err + return nil } -// FindByID 根据ID查找支付订单 -func (p *PaymentRepositoryORM) FindByID(id uint64) (*payment.Payment, error) { +// FindByID 根据ID获取支付订单 +func (r *PaymentRepositoryORM) FindByID(id uint64) (*payment.Payment, error) { payment := &payment.Payment{} - has, err := p.engine.Where(builder.Eq{"id": id}).Get(payment) + has, err := r.engine.ID(id).Get(payment) if err != nil { - p.log.Error(err.Error()) + r.log.Error(err) return nil, err } if !has { - p.log.Errorf("未找到相关数据【%d】", id) - return nil, errors.New("记录不存在") + return nil, errors.New("支付订单不存在") } return payment, nil } -// FindByOrderNo 根据订单号查找支付订单 -func (p *PaymentRepositoryORM) FindByOrderNo(orderNo string) (*payment.Payment, error) { +// FindByOrderNo 根据订单号获取支付订单 +func (r *PaymentRepositoryORM) FindByOrderNo(orderNo string) (*payment.Payment, error) { payment := &payment.Payment{} - has, err := p.engine.Where(builder.Eq{"order_no": orderNo}).Get(payment) + has, err := r.engine.Where(builder.Eq{"order_no": orderNo}).Get(payment) if err != nil { - p.log.Error(err.Error()) + r.log.Error(err) return nil, err } if !has { - p.log.Errorf("未找到相关数据【%s】", orderNo) - return nil, errors.New("记录不存在") + return nil, errors.New("支付订单不存在") } return payment, nil } -// FindByTransactionID 根据微信支付订单号查找支付订单 -func (p *PaymentRepositoryORM) FindByTransactionID(transactionID string) (*payment.Payment, error) { +// FindByTransactionID 根据交易号获取支付订单 +func (r *PaymentRepositoryORM) FindByTransactionID(transactionID string) (*payment.Payment, error) { payment := &payment.Payment{} - has, err := p.engine.Where(builder.Eq{"transaction_id": transactionID}).Get(payment) + has, err := r.engine.Where(builder.Eq{"transaction_id": transactionID}).Get(payment) if err != nil { - p.log.Error(err.Error()) + r.log.Error(err) return nil, err } if !has { - p.log.Errorf("未找到相关数据【%s】", transactionID) - return nil, errors.New("记录不存在") + return nil, errors.New("支付订单不存在") } return payment, nil } -// FindByUserID 根据用户ID查找支付订单列表 -func (p *PaymentRepositoryORM) FindByUserID(userID uint64, page *page.Page) error { - return p.engine.FindAll(page, &payment.Payment{}, builder.And(builder.Eq{"user_id": userID})) - -} - -// FindAll 查询支付订单列表 -func (p *PaymentRepositoryORM) FindAll(page *page.Page, conds []builder.Cond) error { - return p.engine.FindAll(page, &payment.Payment{}, builder.And(conds...)) +// Update 更新支付订单 +func (r *PaymentRepositoryORM) Update(payment *payment.Payment) error { + if _, err := r.engine.ID(payment.ID).Update(payment); err != nil { + r.log.Error(err) + return err + } + return nil } -// Update 更新支付订单 -func (p *PaymentRepositoryORM) Update(payment *payment.Payment) error { - _, err := p.engine.Update(payment) - if err != nil { - p.log.Error(err) +// UpdateStatus 更新支付订单状态 +func (r *PaymentRepositoryORM) UpdateStatus(id uint64, status payment.PaymentStatus) error { + if _, err := r.engine.ID(id).Update(&payment.Payment{Status: status}); err != nil { + r.log.Error(err) + return err } - return err + return nil } // Delete 删除支付订单 -func (p *PaymentRepositoryORM) Delete(id uint64) error { - _, err := p.engine.Delete(&payment.Payment{}, "id = ?", id) - if err != nil { - p.log.Error(err) +func (r *PaymentRepositoryORM) Delete(id uint64) error { + if _, err := r.engine.ID(id).Delete(&payment.Payment{}); err != nil { + r.log.Error(err) + return err } - return err + return nil } diff --git a/internal/infrastructure/persistence/price/price_repo.go b/internal/infrastructure/persistence/price/price_repo.go index a166a22..619f718 100644 --- a/internal/infrastructure/persistence/price/price_repo.go +++ b/internal/infrastructure/persistence/price/price_repo.go @@ -18,7 +18,7 @@ type PriceRepositoryORM struct { var _ price.PriceRepository = (*PriceRepositoryORM)(nil) // NewRepository 创建价格管理仓储 -func NewRepository(engine *xorm_engine.Engine, log logger.New) *PriceRepositoryORM { +func NewPriceRepositoryORM(engine *xorm_engine.Engine, log logger.New) price.PriceRepository { return &PriceRepositoryORM{ engine: engine, log: log("cls:repository:price"), diff --git a/internal/infrastructure/persistence/purchase/purchase_repo.go b/internal/infrastructure/persistence/purchase/purchase_repo.go index 2e1c748..685205e 100644 --- a/internal/infrastructure/persistence/purchase/purchase_repo.go +++ b/internal/infrastructure/persistence/purchase/purchase_repo.go @@ -67,14 +67,14 @@ func (r *PurchaseRepositoryORM) FindColumnsByUserId(userId uint64) ([]*purchase. return purchases, err } -func (r *PurchaseRepositoryORM) FindArticleById(ids ...uint64) ([]*purchase.Purchase, error) { +func (r *PurchaseRepositoryORM) FindArticleById(uid uint64, ids ...uint64) ([]*purchase.Purchase, error) { purchases := make([]*purchase.Purchase, 0) - err := r.engine.Where(builder.In("user_id", ids)).And(builder.Eq{"content_type": purchase.ContentTypeArticle}).Find(&purchases) + err := r.engine.Where(builder.Eq{"user_id": uid}.And(builder.In("content_id", ids)).And(builder.Eq{"content_type": purchase.ContentTypeArticle})).Find(&purchases) return purchases, err } -func (r *PurchaseRepositoryORM) FindColumnById(ids ...uint64) ([]*purchase.Purchase, error) { +func (r *PurchaseRepositoryORM) FindColumnById(uid uint64, ids ...uint64) ([]*purchase.Purchase, error) { purchases := make([]*purchase.Purchase, 0) - err := r.engine.Where(builder.In("user_id", ids)).And(builder.Eq{"content_type": purchase.ContentTypeColumn}).Find(&purchases) + err := r.engine.Where(builder.Eq{"user_id": uid}.And(builder.In("content_id", ids)).And(builder.Eq{"content_type": purchase.ContentTypeColumn})).Find(&purchases) return purchases, err } diff --git a/internal/infrastructure/persistence/user/user_aggregate_repo.go b/internal/infrastructure/persistence/user/user_aggregate_repo.go new file mode 100644 index 0000000..c3a7baf --- /dev/null +++ b/internal/infrastructure/persistence/user/user_aggregate_repo.go @@ -0,0 +1,91 @@ +package user + +import ( + "cls/internal/domain/purchase" + domainUser "cls/internal/domain/user" + "cls/pkg/logger" + "cls/pkg/xorm_engine" + "errors" + "xorm.io/builder" +) + +// UserAggregateRepositoryORM 用户聚合根仓储实现 +type UserAggregateRepositoryORM struct { + engine *xorm_engine.Engine + log logger.Logger +} + +var _ domainUser.UserAggregateRepository = (*UserAggregateRepositoryORM)(nil) + +func NewUserAggregateRepositoryORM(db *xorm_engine.Engine, logger logger.New) domainUser.UserAggregateRepository { + return &UserAggregateRepositoryORM{ + engine: db, + log: logger("panoramic:persistence:user_aggregate"), + } +} + +func (u *UserAggregateRepositoryORM) GetUserAggregate(phone string) (*domainUser.UserAggregate, error) { + // 1. 获取用户信息 + user := &domainUser.User{} + exist, err := u.engine.Where(builder.Eq{"phone": phone}).Get(user) + if err != nil { + u.log.Error("获取用户信息失败", err) + return nil, err + } + if !exist { + return nil, errors.New("用户不存在") + } + + // 2. 获取用户的购买记录 + var purchases []*purchase.Purchase + err = u.engine.Where(builder.Eq{"user_id": user.Id}).Find(&purchases) + if err != nil { + u.log.Error("获取用户购买记录失败", err) + return nil, err + } + + // 3. 构建用户聚合根 + aggregate := domainUser.NewUserAggregate(user) + aggregate.SetPurchases(purchases) + + return aggregate, nil +} + +func (u *UserAggregateRepositoryORM) SaveUserAggregate(aggregate *domainUser.UserAggregate) error { + // 1. 开启事务 + session := u.engine.NewSession() + if err := session.Begin(); err != nil { + u.log.Error("开启事务失败", err) + return err + } + defer func() { + if err := recover(); err != nil { + session.Rollback() + panic(err) + } + }() + + // 2. 更新用户信息 + if _, err := session.Where(builder.Eq{"id": aggregate.GetUser().Id}).Update(aggregate.GetUser()); err != nil { + session.Rollback() + u.log.Error("更新用户信息失败", err) + return err + } + + // 3. 保存购买记录 + for _, p := range aggregate.GetPurchases() { + if _, err := session.Insert(p); err != nil { + session.Rollback() + u.log.Error("保存购买记录失败", err) + return err + } + } + + // 4. 提交事务 + if err := session.Commit(); err != nil { + u.log.Error("提交事务失败", err) + return err + } + + return nil +} diff --git a/internal/interfaces/article/article_handler.go b/internal/interfaces/article/article_handler.go index 21d3198..c2cd9f8 100644 --- a/internal/interfaces/article/article_handler.go +++ b/internal/interfaces/article/article_handler.go @@ -47,8 +47,6 @@ func (a *ArticleHandler) findAll(c *gin.Context) { ePhone, err := a.authMiddleware.DecodeToken(c) if err != nil { a.log.Error(err) - c.AbortWithStatus(http.StatusInternalServerError) - return } err = a.service.Find(ePhone, p, map[string]string{ diff --git a/internal/interfaces/column/column_handler.go b/internal/interfaces/column/column_handler.go new file mode 100644 index 0000000..fc3e662 --- /dev/null +++ b/internal/interfaces/column/column_handler.go @@ -0,0 +1,42 @@ +package column + +import ( + "cls/internal/application/column" + "cls/internal/infrastructure/middleware/auth" + "cls/internal/interfaces" + "cls/pkg/logger" + "github.com/gin-gonic/gin" + "net/http" +) + +type ColumnHandler struct { + service *column.Service + authMiddleware *auth.AuthMiddleware + log logger.Logger +} + +var _ interfaces.Handler = (*ColumnHandler)(nil) + +func NewColumnHandler(service *column.Service, authMiddleware *auth.AuthMiddleware, log logger.New) *ColumnHandler { + return &ColumnHandler{service, authMiddleware, log("cls:interfaces:column")} +} + +func (ch *ColumnHandler) RegisterRouters(app gin.IRouter) { + auth := app.Group("/column") + { + auth.GET("/get", ch.getColumnByName) + } +} + +func (ch *ColumnHandler) getColumnByName(c *gin.Context) { + ePhone, err := ch.authMiddleware.DecodeToken(c) + if err != nil { + ePhone = "" + } + dto, err := ch.service.GetColumn(ePhone, c.Query("title")) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + } else { + c.AbortWithStatusJSON(http.StatusOK, dto) + } +} diff --git a/internal/interfaces/coupon/coupon_handler.go b/internal/interfaces/coupon/coupon_handler.go new file mode 100644 index 0000000..747d171 --- /dev/null +++ b/internal/interfaces/coupon/coupon_handler.go @@ -0,0 +1,68 @@ +package coupon + +import ( + "cls/internal/application/coupon" + middleware "cls/internal/infrastructure/middleware/auth" + "cls/internal/interfaces" + "cls/pkg/logger" + "github.com/gin-gonic/gin" + "net/http" +) + +// Handler 优惠券 HTTP 处理器 +type CouponHandler struct { + couponService *coupon.CouponService + authMiddleware *middleware.AuthMiddleware + log logger.Logger +} + +var _ interfaces.Handler = (*CouponHandler)(nil) + +// NewHandler 创建优惠券 HTTP 处理器 +func NewCouponHandler(couponService *coupon.CouponService, authMiddleware *middleware.AuthMiddleware, log logger.New) *CouponHandler { + return &CouponHandler{ + couponService: couponService, + authMiddleware: authMiddleware, + log: log("cls:interfaces:coupon"), + } +} + +func (c CouponHandler) RegisterRouters(app gin.IRouter) { + coupon := app.Group("/coupon") + { + coupon.GET("/get", c.GetUserCoupons) + coupon.POST("/create", c.CreateCoupon) + } +} + +// CreateCoupon 创建优惠券 +func (h *CouponHandler) CreateCoupon(c *gin.Context) { + var req coupon.CouponDto + if err := c.ShouldBindJSON(&req); err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + if err := h.couponService.CreateCoupon(&req); err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + + } else { + c.AbortWithStatus(http.StatusOK) + } + +} + +// GetUserCoupons 获取用户的优惠券列表 +func (h *CouponHandler) GetUserCoupons(c *gin.Context) { + ePhone, err := h.authMiddleware.DecodeToken(c) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + coupons, err := h.couponService.GetUserCoupons(ePhone) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + } else { + + } + c.JSON(http.StatusOK, coupons) +} diff --git a/internal/interfaces/http/column_handler.go b/internal/interfaces/http/column_handler.go new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/internal/interfaces/http/column_handler.go @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/interfaces/middleware/auth.go b/internal/interfaces/middleware/auth.go new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/internal/interfaces/middleware/auth.go @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/interfaces/order/order_handler.go b/internal/interfaces/order/order_handler.go new file mode 100644 index 0000000..c3f98a9 --- /dev/null +++ b/internal/interfaces/order/order_handler.go @@ -0,0 +1,43 @@ +package order + +import ( + "cls/internal/application/order" + "cls/internal/interfaces" + "cls/pkg/logger" + "github.com/gin-gonic/gin" + "net/http" +) + +type OrderHandler struct { + service *order.OrderService + log logger.Logger +} + +func NewOrderHandler(service *order.OrderService, log logger.New) *OrderHandler { + return &OrderHandler{service, log("cls:interfaces:order")} +} + +var _ interfaces.Handler = (*OrderHandler)(nil) + +func (o *OrderHandler) RegisterRouters(app gin.IRouter) { + auth := app.Group("/order") + { + auth.POST("/create", o.create) + } +} + +func (o *OrderHandler) create(c *gin.Context) { + dto := &order.CreateOrderRequest{} + err := c.ShouldBindJSON(dto) + if err != nil { + o.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + data, err := o.service.CreateOrder(dto) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + } else { + c.AbortWithStatusJSON(http.StatusOK, data) + } +} diff --git a/internal/interfaces/price/price_handler.go b/internal/interfaces/price/price_handler.go new file mode 100644 index 0000000..7044f05 --- /dev/null +++ b/internal/interfaces/price/price_handler.go @@ -0,0 +1,138 @@ +package price + +import ( + "cls/internal/application/price" + "cls/internal/interfaces" + "cls/pkg/logger" + "cls/pkg/util/page" + "github.com/gin-gonic/gin" + "net/http" + "strconv" +) + +type PriceHandler struct { + service *price.PriceService + log logger.Logger +} + +var _ interfaces.Handler = (*PriceHandler)(nil) + +func NewPriceHandler(service *price.PriceService, log logger.New) *PriceHandler { + return &PriceHandler{service, log("cls:interfaces:price")} +} + +func (h *PriceHandler) RegisterRouters(app gin.IRouter) { + auth := app.Group("/price") + { + auth.POST("/create", h.create) + auth.GET("/get", h.getPrice) + auth.PUT("/update", h.update) + auth.GET("/list", h.getPriceList) + auth.GET("/article/:id", h.getArticlePrice) + auth.GET("/column/:id", h.getColumnPrice) + } +} + +func (h *PriceHandler) create(c *gin.Context) { + dto := &price.PriceDto{} + err := c.ShouldBindJSON(dto) + if err != nil { + h.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + err = h.service.SetPrice(dto) + if err != nil { + h.log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + } else { + c.AbortWithStatus(http.StatusOK) + } +} + +func (h *PriceHandler) getPrice(c *gin.Context) { + dto := &price.PriceDto{} + err := c.ShouldBindJSON(dto) + if err != nil { + h.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + price, err := h.service.GetPrice(dto) + if err != nil { + h.log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + c.JSON(http.StatusOK, price) +} + +func (h *PriceHandler) update(c *gin.Context) { + dto := &price.PriceDto{} + err := c.ShouldBindJSON(dto) + if err != nil { + h.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + err = h.service.UpdatePrice(dto) + if err != nil { + h.log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + } else { + c.AbortWithStatus(http.StatusOK) + } +} + +func (h *PriceHandler) getPriceList(c *gin.Context) { + page := &page.Page{} + err := c.ShouldBindJSON(page) + if err != nil { + h.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + err = h.service.GetPriceList(page, map[string]string{ + "search_eq_type": c.Query("search_eq_type"), + }) + if err != nil { + h.log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + c.JSON(http.StatusOK, page) +} + +func (h *PriceHandler) getArticlePrice(c *gin.Context) { + targetId, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + h.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + priceDto := &price.PriceDto{TargetID: uint64(targetId)} + price, err := h.service.GetArticlePrice(priceDto) + if err != nil { + h.log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + c.JSON(http.StatusOK, price) +} + +func (h *PriceHandler) getColumnPrice(c *gin.Context) { + targetId, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + h.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + priceDto := &price.PriceDto{TargetID: uint64(targetId)} + price, err := h.service.GetColumnPrice(priceDto) + if err != nil { + h.log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + c.JSON(http.StatusOK, price) +} diff --git a/internal/modules/column_module.go b/internal/modules/column_module.go new file mode 100644 index 0000000..8e5a712 --- /dev/null +++ b/internal/modules/column_module.go @@ -0,0 +1,16 @@ +package modules + +import ( + service "cls/internal/application/column" + repo "cls/internal/infrastructure/persistence/column" + "cls/internal/interfaces" + "cls/internal/interfaces/column" + "go.uber.org/fx" +) + +var ColumnModule = fx.Module("ColumnModule", + fx.Provide( + interfaces.AsHandler(column.NewColumnHandler), + service.NewService, + repo.NewColumnRepositoryORM, + )) diff --git a/internal/modules/coupon_module.go b/internal/modules/coupon_module.go new file mode 100644 index 0000000..a0e7cbc --- /dev/null +++ b/internal/modules/coupon_module.go @@ -0,0 +1,16 @@ +package modules + +import ( + service "cls/internal/application/coupon" + repo "cls/internal/infrastructure/persistence/coupon" + "cls/internal/interfaces" + "cls/internal/interfaces/coupon" + "go.uber.org/fx" +) + +var CouponModule = fx.Module("couponModule", + fx.Provide( + interfaces.AsHandler(coupon.NewCouponHandler), + service.NewCouponService, + repo.NewCouponRepository, + )) diff --git a/internal/modules/module.go b/internal/modules/module.go index 4c3178e..4bc1fbc 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -2,7 +2,12 @@ package modules import ( "cls/internal/domain/article" + "cls/internal/domain/column" + "cls/internal/domain/coupon" "cls/internal/domain/free_trial" + "cls/internal/domain/order" + "cls/internal/domain/payment" + "cls/internal/domain/price" "cls/internal/domain/purchase" "cls/internal/domain/user" "cls/pkg/logger" @@ -17,6 +22,10 @@ var Module = fx.Module("cls", ArticleModule, //文章模块 PurchaseModule, //购买模块 FreeTrialModule, + PriceModule, + ColumnModule, + OrderModule, + CouponModule, ), fx.Options(), fx.Invoke(registerModels), @@ -35,6 +44,11 @@ func registerModels(engine *xorm_engine.Engine, logger logger.New) { &user.User{}, &free_trial.FreeTrial{}, &purchase.Purchase{}, + &price.Price{}, + &column.Column{}, + &order.Order{}, + &payment.Payment{}, + &coupon.Coupon{}, ); err != nil { log.Error(err) } diff --git a/internal/modules/order_module.go b/internal/modules/order_module.go new file mode 100644 index 0000000..bb43a79 --- /dev/null +++ b/internal/modules/order_module.go @@ -0,0 +1,19 @@ +package modules + +import ( + service "cls/internal/application/order" + repo "cls/internal/infrastructure/persistence/order" + "cls/internal/interfaces" + "cls/internal/interfaces/order" + "go.uber.org/fx" +) + +// OrderModule 订单模块 +var OrderModule = fx.Module("order", + fx.Provide( + interfaces.AsHandler(order.NewOrderHandler), + repo.NewOrderRepositoryORM, + repo.NewAggregateRepositoryORM, + service.NewOrderService, + ), +) diff --git a/internal/modules/price_module.go b/internal/modules/price_module.go new file mode 100644 index 0000000..7941e49 --- /dev/null +++ b/internal/modules/price_module.go @@ -0,0 +1,16 @@ +package modules + +import ( + service "cls/internal/application/price" + repo "cls/internal/infrastructure/persistence/price" + "cls/internal/interfaces" + "cls/internal/interfaces/price" + "go.uber.org/fx" +) + +var PriceModule = fx.Module("PriceModule", + fx.Provide( + interfaces.AsHandler(price.NewPriceHandler), + service.NewPriceService, + repo.NewPriceRepositoryORM, + )) diff --git a/internal/modules/user_module.go b/internal/modules/user_module.go index 9c587ff..c68ba41 100644 --- a/internal/modules/user_module.go +++ b/internal/modules/user_module.go @@ -15,5 +15,6 @@ var UserModule = fx.Module("UserModule", fx.Provide( service.NewUserService, repo.NewUserRepositoryORM, + repo.NewUserAggregateRepositoryORM, ), ) diff --git a/pkg/response/response.go b/pkg/response/response.go new file mode 100644 index 0000000..a3c2442 --- /dev/null +++ b/pkg/response/response.go @@ -0,0 +1,50 @@ +package response + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +// Response 统一响应结构 +type Response struct { + Code int `json:"code"` // 业务码 + Message string `json:"message"` // 提示信息 + Data interface{} `json:"data,omitempty"` // 数据 +} + +// Success 成功响应 +func Success(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: 0, + Message: "success", + Data: data, + }) +} + +// Error 错误响应 +func Error(c *gin.Context, code int, message string) { + c.JSON(code, Response{ + Code: code, + Message: message, + }) +} + +// ValidationError 参数验证错误响应 +func ValidationError(c *gin.Context, message string) { + Error(c, http.StatusBadRequest, message) +} + +// ServerError 服务器错误响应 +func ServerError(c *gin.Context, message string) { + Error(c, http.StatusInternalServerError, message) +} + +// UnauthorizedError 未授权错误响应 +func UnauthorizedError(c *gin.Context) { + Error(c, http.StatusUnauthorized, "未授权访问") +} + +// NotFoundError 资源不存在错误响应 +func NotFoundError(c *gin.Context) { + Error(c, http.StatusNotFound, "资源不存在") +}