feat(1.0):除微信支付外,第一版功能完成

developing
张帅 2 weeks ago
parent a36eff4595
commit 0a2e118872

@ -0,0 +1,21 @@
all: frontend build tar
image: frontend docker-build save-image
TAG=v1.0
frontend:
cd cls && yarn run build
tar:
tar -czf cls-h5.$(TAG).tar.gz cls config
docker-build:
docker build -t k8s.dingshudata.com/cmcc-panoramic-application:$(TAG) .
save-image:
docker save -o cmcc.tar k8s.dingshudata.com/cmcc-panoramic-application
build:export CGO_ENABLED=0
build:export GOOS=linux
build:export GOARCH=amd64
build:
go build -ldflags -s -tags="jsoniter nomsgpack" .

1786
cls/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@ const routes: Routes = [
pathMatch: 'full'
},
{
path:'home',
path: 'home',
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{

@ -1,21 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import {inject, Injectable, Injector} from '@angular/core';
import {
HttpRequest,
HttpHandler,
@ -11,39 +11,38 @@ import { AuthConfigConsts, tokenNotExpired } from "../../mine/auth.jwt";
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor() {}
//从请求头获取token
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = sessionStorage.getItem('token');
console.log('当前 token:', token);
console.log('请求:', req.url);
const token = localStorage.getItem('token');
console.log(token)
let authHeader = req.headers;
if (token != null && tokenNotExpired()) {
console.log("is valid")
const bearer = AuthConfigConsts.HEADER_PREFIX_BEARER + token;
authHeader = authHeader.set(AuthConfigConsts.DEFAULT_HEADER_NAME, bearer);
const authReq = req.clone({headers: authHeader});
return next.handle(authReq)
} else {
const authReq = req.clone({headers: authHeader});
return next.handle(authReq).pipe(
tap((event) => {
// 如果是 HTTP 响应
if (event instanceof HttpResponse) {
// 从响应头中获取 Authorization token
const authToken = event.headers.get('Authorization');
if (authToken && (!token || token !== authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, ''))) {
// 去掉 Bearer 前缀
const newToken = authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, '');
// 保存到 sessionStorage
localStorage.setItem('token', newToken);
}
}
})
);
}
const authReq = req.clone({headers: authHeader});
return next.handle(authReq).pipe(
tap((event) => {
// 如果是 HTTP 响应
if (event instanceof HttpResponse) {
// 从响应头中获取 Authorization token
const authToken = event.headers.get('Authorization');
if (authToken && (!token || token !== authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, ''))) {
console.log('从响应中获取到新的 token:', authToken);
// 去掉 Bearer 前缀
const newToken = authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, '');
// 保存到 sessionStorage
sessionStorage.setItem('token', newToken);
}
}
})
);
}
private isGuestToken(token: string): boolean {
@ -51,10 +50,8 @@ export class AuthInterceptor implements HttpInterceptor {
// 解析 JWT token
const payload = JSON.parse(atob(token.split('.')[1]));
const isGuest = !payload.phone;
console.log('Token payload:', payload); // 添加日志
return isGuest;
} catch (error) {
console.error('Error parsing token:', error); // 添加错误日志
return true; // 解析失败时默认为游客
}
}

@ -11,7 +11,7 @@
<!-- 文章信息 -->
<div class="article-info">
<div class="article-preview">
<img [src]="''" class="preview-img" alt="文章封面">
<!-- <img [src]="''" class="preview-img" alt="文章封面">-->
<div class="preview-text">
<p class="title">{{article!.title}}</p>
<p class="desc">{{article!.brief}}</p>
@ -26,11 +26,11 @@
<div class="content">
<div class="left">
<span class="type-text">单篇解锁</span>
<span class="discount-tag">{{(price?.discount || 0.3) * 10}}折</span>
<span class="discount-tag">{{(price.discount || 0.3) * 10}}折</span>
</div>
<div class="right">
<span class="original-price">¥{{price?.amount || 1888}}</span>
<span class="current-price">¥{{getDiscountPrice(this.price.amount,this.price.discount)|| 566.4}}</span>
<span class="original-price">¥{{price.amount/100 || 1888}}</span>
<span class="current-price">¥{{getDiscountPrice(price.amount,price.discount)|| 566.4}}</span>
<ion-icon name="checkmark-circle" class="select-icon"></ion-icon>
</div>
</div>
@ -50,12 +50,12 @@
<!-- <ion-icon name="chevron-forward-outline"></ion-icon>-->
<!-- </div>-->
<!-- </div>-->
<app-coupon-list [coupons]="coupons" [amount]="price.amount" (couponSelected)="couponSelected($event)" ></app-coupon-list>
<app-coupon-list [coupons]="coupons" [amount]="this.getDiscountPrice(this.price.amount,this.price.discount)" (couponSelected)="couponSelected($event)" ></app-coupon-list>
<!-- 底部固定容器 -->
<div class="bottom-fixed-wrapper">
<!-- 提交按钮 -->
<ion-button expand="block" class="submit-btn" (click)="submitOrder()">
确认支付 ¥{{getDiscountPriceAndCoupon(price!.amount, price!.discount)}}
<ion-button expand="block" class="submit-btn" [disabled]="getDiscountPriceAndCoupon(price.amount, price.discount) <= 0" (click)="submitOrder()">
确认支付 ¥{{getDiscountPriceAndCoupon(price.amount, price.discount)}}
</ion-button>
<app-lc-fixed-bar></app-lc-fixed-bar>

@ -38,8 +38,6 @@ export class ArticleBuyPage implements OnInit {
}
this.article = article;
this.price = price;
console.log(this.price)
}
ngOnInit() {
@ -48,28 +46,23 @@ export class ArticleBuyPage implements OnInit {
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折
return +(originalPrice/100 * discount).toFixed(1) ; // 3折
}
getDiscountPriceAndCoupon(originalPrice: number,discount:number): number {
if (!originalPrice) return 0;
return +(originalPrice * discount).toFixed(1) - this.couponAmount; // 3折
return +(originalPrice/100 * discount).toFixed(1) - this.couponAmount; // 3折
}
couponSelected(coupon:Coupon|null){
this.selectCoupon = coupon;
console.log(coupon)
if(coupon) {
this.couponAmount = coupon.value;
} else {

@ -1,7 +1,7 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/home"></ion-back-button>
<ion-back-button text="" defaultHref="/home"></ion-back-button>
</ion-buttons>
<ion-title>文章详情</ion-title>
</ion-toolbar>

@ -3,6 +3,9 @@
--background: #fff;
}
}
ion-back-button {
--color: #333333;
}
.article-title {
font-size: 24px;

@ -11,12 +11,12 @@
<!-- 专栏信息 -->
<div class="column-info">
<div class="icon-circle" [ngClass]="getIconImage(column!.title)?.replace('.png', '')" >
<img [src]="'assets/home/' + getIconImage(column!.title)" [alt]="column?.title">
<div class="icon-circle" [ngClass]="getIconImage(column.title).replace('.png', '')" >
<img [src]="'assets/home/' + getIconImage(column.title)" [alt]="column.title">
</div>
<div class="column-text">
<h2>{{column?.title}}</h2>
<p>{{column?.brief}}</p>
<h2>{{column.title}}</h2>
<p>{{column.brief}}</p>
</div>
</div>
@ -25,48 +25,48 @@
<div class="price-row">
<!-- 1个月选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '1'" (click)="selectPeriod('1')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.oneMonthPrice || 0, 1)}}/天</div>
<div class="daily-price">¥{{getDailyPrice(columnPrice.oneMonthPrice || 0, 1)}}/天</div>
<div class="period">1个月</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.oneMonthPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.oneMonthPrice}}</span>
<span class="price">¥{{getDiscountPrice(columnPrice.oneMonthPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice.oneMonthPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
<div class="discount-tag">{{(columnPrice.discount || 0.3) * 10}}折</div>
</div>
<!-- 3个月选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '3'" (click)="selectPeriod('3')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.threeMonthsPrice || 0, 3)}}/天</div>
<div class="daily-price">¥{{getDailyPrice(columnPrice.threeMonthsPrice || 0, 3)}}/天</div>
<div class="period">3个月</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.threeMonthsPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.threeMonthsPrice}}</span>
<span class="price">¥{{getDiscountPrice(columnPrice.threeMonthsPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice.threeMonthsPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
<div class="discount-tag">{{(columnPrice.discount || 0.3) * 10}}折</div>
</div>
</div>
<div class="price-row">
<!-- 6个月选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '6'" (click)="selectPeriod('6')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.sixMonthsPrice || 0, 6)}}/天</div>
<div class="daily-price">¥{{getDailyPrice(columnPrice.sixMonthsPrice || 0, 6)}}/天</div>
<div class="period">6个月</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.sixMonthsPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.sixMonthsPrice}}</span>
<span class="price">¥{{getDiscountPrice(columnPrice.sixMonthsPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice.sixMonthsPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
<div class="discount-tag">{{(columnPrice.discount || 0.3) * 10}}折</div>
</div>
<!-- 1年选项 -->
<div class="price-card" [class.selected]="selectedPeriod === '12'" (click)="selectPeriod('12')">
<div class="daily-price">¥{{getDailyPrice(columnPrice?.oneYearPrice || 0, 12)}}/天</div>
<div class="daily-price">¥{{getDailyPrice(columnPrice.oneYearPrice || 0, 12)}}/天</div>
<div class="period">1年</div>
<div class="total-price">
<span class="price">¥{{getDiscountPrice(columnPrice?.oneYearPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice?.oneYearPrice}}</span>
<span class="price">¥{{getDiscountPrice(columnPrice.oneYearPrice || 0,columnPrice.discount)}}</span>
<span class="original-price">¥{{columnPrice.oneYearPrice}}</span>
</div>
<div class="discount-tag">{{(columnPrice?.discount || 0.3) * 10}}折</div>
<div class="discount-tag">{{(columnPrice.discount || 0.3) * 10}}折</div>
</div>
</div>
</div>
@ -87,7 +87,7 @@
<!-- <div class="label">优惠券</div>-->
<!-- <div class="no-coupon">无可用优惠券 ></div>-->
<!-- </div>-->
<app-coupon-list [coupons]="coupons" [amount]="columnPrice.amount" (couponSelected)="couponSelected($event)" ></app-coupon-list>
<app-coupon-list [coupons]="coupons" [amount]="this.getDiscountPriceAndCoupon(this.getSelectedPrice(),this.columnPrice.discount)" (couponSelected)="couponSelected($event)" ></app-coupon-list>
<!-- 底部固定容器 -->
<div class="bottom-fixed-wrapper">
<!-- 提交按钮 -->

@ -6,6 +6,7 @@ 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";
import {OrderType} from "../../shared/model/order";
@Component({
selector: 'app-column-buy',
@ -49,18 +50,18 @@ export class ColumnBuyPage implements OnInit {
// 计算每日价格
getDailyPrice(monthPrice: number, months: number): number {
if (!monthPrice) return 0;
return +(monthPrice / (months * 30)).toFixed(1);
return +(monthPrice/100 / (months * 30)).toFixed(1);
}
// 获取折扣价格
getDiscountPrice(originalPrice: number,discount:number): number {
if (!originalPrice) return 0;
return +(originalPrice * discount).toFixed(1) ; // 3折
return +(originalPrice/100 * discount).toFixed(1) ; // 3折
}
getDiscountPriceAndCoupon(originalPrice: number,discount:number): number {
if (!originalPrice) return 0;
return +( originalPrice* discount).toFixed(1) - this.couponAmount; // 3折
return +( originalPrice/100 * discount).toFixed(1) ; // 3折
}
// 选择订阅周期
@ -87,13 +88,6 @@ export class ColumnBuyPage implements OnInit {
}
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;
})
}
@ -108,6 +102,9 @@ export class ColumnBuyPage implements OnInit {
// 提交订单
submitOrder() {
// TODO: 实现订单提交逻辑
this.navCtrl.navigateForward('/home/confirm-order',{
state:{order:{targetId:this.column.id,type:OrderType.OrderTypeColumn,amount:this.getDiscountPriceAndCoupon(this.getSelectedPrice()|| 0,this.columnPrice.discount) -this.couponAmount}}
})
}
// 显示更多优惠弹窗

@ -1,35 +1,40 @@
<ion-segment (ionChange)="ionChange($event)">
<ion-segment-button value="new" content-id="new">
<ion-segment [(ngModel)]="currentSelect" (ionChange)="ionChange($event)">
<ion-segment-button value="new">
<ion-label>最新</ion-label>
</ion-segment-button>
<ion-segment-button value="unlock" content-id="unlock">
<ion-segment-button value="unlock">
<ion-label>已解锁</ion-label>
</ion-segment-button>
<ion-segment-button value="free" content-id="free">
<ion-segment-button value="free">
<ion-label>免费试读</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="new">
<div class="segment-content">
<div *ngIf="currentSelect === 'new'">
<ion-list>
<app-article-item [article]="item" *ngFor="let item of data" ></app-article-item>
<app-article-item [article]="item" [username]="username" *ngFor="let item of data"></app-article-item>
</ion-list>
</ion-segment-content>
<ion-segment-content id="unlock">
<ion-list>
<app-article-item [article]="item" *ngFor="let item of data" ></app-article-item>
</div>
<div *ngIf="currentSelect === 'unlock'">
<div class="empty-state" *ngIf="!username">
<ion-icon name="log-in-outline"></ion-icon>
<p>请登录后查看</p>
</div>
<ion-list *ngIf="username">
<app-article-item [article]="item" [username]="username" *ngFor="let item of data"></app-article-item>
</ion-list>
</ion-segment-content>
<ion-segment-content id="free">
<ion-list>
<app-article-item [article]="item" *ngFor="let item of data" ></app-article-item>
</div>
<div *ngIf="currentSelect === 'free'">
<ion-list>
<app-article-item [article]="item" [username]="username" *ngFor="let item of data"></app-article-item>
</ion-list>
</ion-segment-content>
</ion-segment-view>
</div>
</div>
<app-lc-fixed-bar *ngIf="data.length != 0"></app-lc-fixed-bar>
<ion-infinite-scroll (ionInfinite)="onIonInfinite($event)">
<ion-infinite-scroll-content

@ -71,3 +71,24 @@ ion-segment {
transform: scaleX(1);
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
ion-icon {
font-size: 48px;
color: #999;
margin-bottom: 16px;
}
p {
color: #999;
font-size: 14px;
margin: 0;
}
}

@ -2,6 +2,9 @@ import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {Article} from "../../../shared/model/article";
import {InfiniteScrollCustomEvent} from "@ionic/angular";
import {HomeService} from "../../home.service";
import {getGiftCount, getUser} from "../../../mine/mine.service";
import {Subject, Subscription} from "rxjs";
import {NavigationEnd, Router} from "@angular/router";
@Component({
selector: 'app-article-content',
@ -13,16 +16,20 @@ export class ArticleContentComponent implements OnInit {
searchParams: { [param: string]: any } = {
page: 0,
size: 10,
search_eq_class:""
};
hasMore: boolean = true;
@Input() className:string = ""
username:string = ""
data:Article[] = []
currentSelect = "new"
constructor(private homeService:HomeService,
private cdr:ChangeDetectorRef) { }
ngOnInit() {
this.getData();
this.getUsername();
}
ionChange(e:any){
this.searchParams['page'] = 0
@ -43,6 +50,14 @@ export class ArticleContentComponent implements OnInit {
return this.getNewData()
}
}
getUsername(){
getUser().subscribe((res)=>{
this.username = res.username
this.cdr.detectChanges();
})
}
getNewData() {
this.homeService.list(this.searchParams).subscribe(res=>{
@ -55,14 +70,21 @@ export class ArticleContentComponent implements OnInit {
})
}
getUnLockData() {
this.homeService.unlockList(this.searchParams).subscribe(res=>{
if(res.items.length > 0) {
this.data = this.data.concat(res.items)
this.searchParams['page'] = this.searchParams['page']+1;
this.hasMore = res.items.length === this.searchParams['size'];
this.cdr.detectChanges();
this.getUsername()
if(this.username!= ""){
if(this.className != "") {
this.searchParams['search_eq_class'] = this.className
}
})
this.homeService.unlockList(this.searchParams).subscribe(res=>{
if(res.items.length > 0) {
this.data = this.data.concat(res.items)
this.searchParams['page'] = this.searchParams['page']+1;
this.hasMore = res.items.length === this.searchParams['size'];
this.cdr.detectChanges();
}
})
}
}
getFreeData() {
this.homeService.freeList(this.searchParams).subscribe(res=>{

@ -13,6 +13,6 @@
</div>
<div class="lock-mask" *ngIf="!article.unlock">
<button class="unlock-button" (click)="onUnlock()">立即解锁</button>
<button class="unlock-button" [class.noCount]="freeReadCount==0" [disabled]="freeReadCount==0">赠送({{freeReadCount}}次)</button>
<button class="unlock-button" (click)="unlockArticle($event)" [class.noCount]="getFreeReadCount()<1" [disabled]="getFreeReadCount()<1">赠送({{getFreeReadCount()}}次)</button>
</div>
</div>

@ -3,6 +3,8 @@
background: #fff;
position: relative;
overflow: hidden;
margin-bottom: 20px;
border-bottom: #8B8D93 1px solid ;
}
.article-content {

@ -1,32 +1,36 @@
import {Component, Input, OnInit, Output, EventEmitter} from '@angular/core';
import {AfterViewInit, ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {Article} from "../../../shared/model/article";
import {NavController} from "@ionic/angular";
import {getUser} from "../../../mine/mine.service";
import {AlertController, NavController, ToastController} from "@ionic/angular";
import {getGiftCount, getUser, useGiftCount} from "../../../mine/mine.service";
import {HomeService} from "../../home.service";
import { AlertController } from '@ionic/angular';
import {debounceTime, Subject, takeUntil} from "rxjs";
@Component({
selector: 'app-article-item',
templateUrl: './article-item.component.html',
styleUrls: ['./article-item.component.scss'],
standalone:false,
standalone: false,
})
export class ArticleItemComponent implements OnInit {
@Input() article!:Article
@Input() lock:boolean = true;
freeReadCount:number=10;
constructor(private navCtrl: NavController,
private alertCtrl:AlertController,
private homeService:HomeService) {
getUser().subscribe((res)=>{
if(res.username != "") {
this.freeReadCount = parseInt(localStorage.getItem("giftCount")!)
}
})
export class ArticleItemComponent implements OnInit,AfterViewInit {
@Input() article!: Article
@Input() lock: boolean = true;
@Input() username: string = ""
freeReadCount: number = -1;
constructor(private navCtrl: NavController,
private alertCtrl: AlertController,
private cdr: ChangeDetectorRef,
private toastCtrl: ToastController,
private homeService: HomeService) {
}
ngOnInit() {}
ngOnInit() {
}
ngAfterViewInit() {
}
onUnlock() {
getUser().subscribe((res)=>{
@ -38,26 +42,93 @@ export class ArticleItemComponent implements OnInit {
buttons: ['确定']
}).then(alert => alert.present());
} else {
this.homeService.getArticlePrice(this.article.eventId).subscribe((res)=>{
console.log(res)
console.log(this.article)
this.homeService.getArticlePrice(this.article.eventId).subscribe((res) => {
this.navCtrl.navigateForward('/home/article-buy', {
state:{article:this.article,price:res}
state: {article: this.article, price: res}
});
})
}
this.username = res.username
})
}
getFreeReadCount():number{
getGiftCount().subscribe((res)=>{
this.freeReadCount = res
})
return this.freeReadCount
}
detail(){
console.log("跳转")
if(this.article.unlock && this.article.stocks != "" && this.article.content != "") {
console.log("跳转")
console.log(this.article)
unlockArticle(event: any) {
event.stopPropagation()
getUser().subscribe((res)=>{
if(res.username == "") {
// 未登录,显示提示框
this.alertCtrl.create({
header: '提示',
message: '请先登录后再进行操作',
buttons: ['确定']
}).then(alert => alert.present());
} else {
this.alertCtrl.create({
header: '提示',
message: `您剩余${this.freeReadCount}确定使用1次解锁该文章吗解锁后剩余赠送次数为${this.freeReadCount - 1}`,
buttons: [
{
text: '确认',
role: 'cancel',
handler: () => {
this.homeService.unlockArticle({eventId: this.article.eventId} as Article).subscribe((res) => {
this.toastCtrl.create({
message: '解锁成功!',
duration: 2000,
color: 'success',
position: 'top',
cssClass: 'ion-text-center'
}).then(toast => toast.present());
this.article.unlock = true;
this.article.content = res.content;
this.article.stocks = res.stocks;
this.freeReadCount-- ;
this.cdr.detectChanges()
useGiftCount()
}, error => {
this.toastCtrl.create({
message: '解锁失败,请稍后重试',
duration: 2000,
color: 'danger',
position: 'top',
cssClass: 'ion-text-center'
}).then(toast => toast.present());
})
}
},
{text: '取消',}
]
}).then((v1) => {
v1.present()
});
}
this.username = res.username
})
}
detail() {
if (this.article.unlock && this.article.stocks != "" && this.article.content != "") {
this.navCtrl.navigateForward('/home/article-detail', {
state:{article:this.article}
state: {article: this.article}
});
}
}
formatReleaseTime(releaseDate: string, releaseTime: string): string {
const date = new Date(releaseDate);
const time = releaseTime || '00:00:00';
@ -92,4 +163,6 @@ export class ArticleItemComponent implements OnInit {
return `${formatShortDate(date)} ${time}`;
}
}
}

@ -8,11 +8,6 @@
</div>
<ion-radio-group [(ngModel)]="selectedCouponId" (ionChange)="onCouponSelect($event)">
<!-- 不使用优惠券选项 -->
<div class="coupon-item no-use-coupon" [class.active]="selectedCouponId === 'no-use'">
</div>
<div class="coupon-list">
<div class="coupon-item">
<ion-radio value="no-use"
@ -24,10 +19,10 @@
<div class="coupon-item"
*ngFor="let coupon of coupons"
[class.active]="isCouponSelected(coupon)"
[class.inactive]="!coupon.canUse"
[class.expired]="coupon.status === 'EXPIRED'">
[class.inactive]="coupon.minAmount > amount"
[class.expired]="isCouponAvailable(coupon)">
<ion-radio [value]="coupon.id"
[disabled]="!coupon.canUse"
[disabled]="coupon.minAmount > amount"
slot="start"
mode="md"></ion-radio>
<div class="coupon-left">

@ -17,7 +17,6 @@ export class CouponListComponent implements OnInit,AfterViewInit {
@Output() couponSelected = new EventEmitter<Coupon|null>();
constructor(private homeService: HomeService) {
console.log(this.amount)
}
ngAfterViewInit() {
}
@ -26,10 +25,8 @@ export class CouponListComponent implements OnInit,AfterViewInit {
}
onCouponSelect(event: any) {
console.log(event)
const selectedCoupon = this.coupons.find(c => c.id === event.detail.value);
if (selectedCoupon && selectedCoupon.canUse) {
console.log(selectedCoupon)
if (selectedCoupon && selectedCoupon.minAmount < this.amount) {
this.couponAmount = selectedCoupon.value;
this.couponSelected.emit(selectedCoupon);
} else {

@ -25,7 +25,6 @@ export class ConfirmOrderPage implements OnInit {
) {
const navigation = this.router.getCurrentNavigation();
const order = navigation?.extras?.state?.['order'];
console.log(order)
if (!order) {
this.navCtrl.back();
return;

@ -12,6 +12,5 @@
</div>
<app-home-icons></app-home-icons>
<app-article-content></app-article-content>
<app-lc-fixed-bar></app-lc-fixed-bar>
</ion-content>

@ -1,8 +1,5 @@
import {ChangeDetectorRef, Component, OnInit} from '@angular/core';
import {InfiniteScrollCustomEvent} from "@ionic/angular";
import {Article} from "../shared/model/article";
import {HomeService} from "./home.service";
import {getUser} from "../mine/mine.service";
import { Component, OnInit} from '@angular/core';
import { getUser} from "../mine/mine.service";
@Component({
selector: 'app-home',
@ -13,16 +10,23 @@ import {getUser} from "../mine/mine.service";
export class HomePage implements OnInit{
constructor(private homeService:HomeService,
private cdr:ChangeDetectorRef) {
constructor(
) {
getUser().subscribe((res)=>{
console.log(res)
})
}
ngOnInit() {
// this.routerSub = this.router.events.subscribe(event => {
// if (event instanceof NavigationEnd) {
// if(event.url == "/home") {
// getGiftCount().subscribe((res)=>{
//
// })
// }
// }
// });
}

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {HttpClient, HttpParams} from "@angular/common/http";
import {map, Observable} from "rxjs";
import {Article} from "../shared/model/article";
import {extractData, Page} from "../shared/model/page";
@ -29,6 +29,9 @@ export class HomeService {
map(response => extractData<Article>(response)),
)
}
unlockArticle(article:Article):Observable<Article> {
return this.http.post<Article>('/api/article/unlock-article', article)
}
getAllClass():Observable<string[]>{
return this.http.get<string[]>('/api/article/class')
}
@ -49,7 +52,6 @@ export class HomeService {
return this.http.post<any>('/api/order/create',order)
}
getCouponList():Observable<Array<Coupon>> {
console.log("开始请求")
return this.http.get<Array<Coupon>>('/api/coupon/get',{})
}
}

@ -8,5 +8,5 @@
</ion-header>
<ion-content [fullscreen]="true">
<img [src]="'assets/class_des/' + getIconImage(column!.title)" [alt]="column?.title">
<img [src]="'assets/class_des/' + getIconImage(column.title)" [alt]="column.title">
</ion-content>

@ -23,22 +23,21 @@
</div>
<div class="stats-row">
<div class="stat-item">
<span class="number">19.2w</span>
<span class="number">{{ column?.followNum ? column!.followNum : 0 }}</span>
<span class="label">人关注</span>
</div>
<div class="stat-item">
<span class="number">19.2w</span>
<span class="number">{{ column?.purchaseNum ? column!.purchaseNum : 0 }}</span>
<span class="label">人购买</span>
</div>
</div>
<div class="intro-section">
<h2>这是消息标题</h2>
<p>这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容</p>
</div>
<!-- <div class="intro-section">-->
<!-- <h2>这是消息标题</h2>-->
<!-- <p>这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容这是消息内容</p>-->
<!-- </div>-->
</div>
<app-article-content></app-article-content>
<app-lc-fixed-bar></app-lc-fixed-bar>
<app-article-content [className]="name"></app-article-content>
</ion-content>
<!-- 底部固定按钮 -->
@ -47,5 +46,5 @@
<p class="p1">盘中宝</p>
<p class="p2">重磅信息挖掘</p>
</div>
<button class="buy-btn" (click)="onBuyClick()">立即订购</button>
<button class="buy-btn" (click)="onBuyClick()">立即订购</button>
</div>

@ -35,9 +35,7 @@ export class SpecialColumnPage implements OnInit {
getColumnData() {
this.homeService.getColumnByName(this.name).subscribe(data => {
console.log(data)
this.column = data;
console.log(this.column)
})
}

@ -112,7 +112,7 @@ export class JwtHelper {
*/
export function tokenNotExpired(tokenName = AuthConfigConsts.DEFAULT_TOKEN_NAME, jwt?: string): boolean {
const token: string | null = jwt || sessionStorage.getItem(tokenName);
const token: string | null = jwt || localStorage.getItem(tokenName);
const jwtHelper = new JwtHelper();

@ -20,12 +20,10 @@ export class CaptchaModalComponent {
}
captchaEvents = {
confirm: (point: SlidePoint, clear: (fn: Function) => void) => {
console.log('用户滑动位置:', point);
if(point.x < this.x+5 && point.x >this.x-5 ) {
this.modalCtrl.dismiss(this.x)
}
clear(() => {
console.log('滑动验证失败,清除验证码');
});
}
};

@ -43,6 +43,12 @@
<div class="register-hint">
未注册的手机号,登录时将自动注册
</div>
<!-- 隐私政策 -->
<div class="privacy-hint">
登录即代表您已同意
<a routerLink="/mine/privacy">《用户隐私政策》</a>
</div>
</form>
</div>
</div>

@ -127,4 +127,16 @@
height: auto;
display: block;
}
}
.privacy-hint {
text-align: center;
font-size: 12px;
color: #999;
margin-top: 16px;
a {
color: #ee0a24;
text-decoration: none;
}
}

@ -83,7 +83,6 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit {
// 调用滑动验证服务
this.mineService.getSlideCaptcha(phone).subscribe( (res) => {
console.log("chuangjian")
this.slideConfigValid.x = res.thumbX;
this.slideConfigValid.y = res.thumbY;
res.thumbX = 0
@ -95,15 +94,11 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit {
},
})
modal.catch((res)=>{
console.log(res)
})
modal.then((res)=>{
console.log(res)
res.present()
res.onDidDismiss().then((res)=>{
console.log(res)
if(res.data < this.slideConfigValid.x+5 && res.data >this.slideConfigValid.x-5 ) {
console.log("发送验证码")
this.sendSmsCode()
}
})
@ -123,17 +118,13 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit {
return;
}
console.log('发送短信验证码,手机号:', phone);
// 直接发送短信验证码
this.mineService.sendSmsCaptcha(phone, '').subscribe({
next: (response) => {
console.log('短信验证码发送成功:', response);
this.startCountdown();
this.showToast('验证码已发送');
},
error: (error) => {
console.error('发送验证码失败:', error);
let errorMessage = '发送失败,请重试';
if (error && error.error) {
@ -163,15 +154,12 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit {
loginRequest.subscribe({
next: (response) => {
console.log('登录成功:', response);
this.showToast('登录成功');
sessionStorage.setItem("token",response)
localStorage.setItem("token",response)
this.navCtrl.back()
},
error: (error) => {
console.error('登录失败:', error);
let errorMessage = '登录失败,请重试';
if (error && error.error) {
if (typeof error.error === 'string') {
errorMessage = error.error;
@ -179,7 +167,6 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit {
errorMessage = error.error.error;
}
}
this.showToast(errorMessage);
}
});

@ -1,21 +1,27 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MinePage } from './mine.page';
const routes: Routes = [
{
path: '',
component: MinePage
}, {
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MinePage } from './mine.page';
const routes: Routes = [
{
path: '',
component: MinePage
},
{
path: 'login',
loadChildren: () => import('./login/login.module').then( m => m.LoginPageModule)
},
{
path:'privacy',
loadChildren:() => import('./privacy/privacy.module').then(m=>m.PrivacyPageModule)
},
];
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class MinePageRoutingModule {}
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class MinePageRoutingModule {}

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { IonicModule } from '@ionic/angular';
import { MinePageRoutingModule } from './mine-routing.module';
@ -13,9 +13,9 @@ import { MinePage } from './mine.page';
CommonModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
IonicModule,
MinePageRoutingModule
],
declarations: [MinePage]
})

@ -5,43 +5,42 @@
</ion-header>
<ion-content class="ion-padding">
<div class="profile-wrapper" *ngIf="isLoggedIn">
<ion-list>
<ion-item class="profile-header">
<ion-avatar slot="start">
<img [src]="'assets/default-avatar.png'" alt="avatar">
</ion-avatar>
<ion-label>
<h2>{{userInfo?.username || '未设置昵称'}}</h2>
<p>{{userInfo?.phone || '未绑定手机号'}}</p>
<h2>{{userInfo?.phone || '未绑定手机号'}}</h2>
</ion-label>
</ion-item>
<ion-item-divider>
<ion-label>我的服务</ion-label>
<ion-label>我的优惠券</ion-label>
</ion-item-divider>
<ion-item button detail="true">
<ion-icon name="star" slot="start" color="warning"></ion-icon>
<ion-label>我的收藏</ion-label>
</ion-item>
<ion-item button detail="true">
<ion-icon name="time" slot="start" color="primary"></ion-icon>
<ion-label>浏览历史</ion-label>
</ion-item>
<ion-item button detail="true">
<ion-icon name="settings" slot="start" color="medium"></ion-icon>
<ion-label>设置</ion-label>
</ion-item>
<div class="coupon-list">
<div class="coupon-item" *ngFor="let coupon of coupons">
<div class="coupon-left">
<div class="amount">
<span class="symbol">¥</span>{{coupon.value}}
</div>
<div class="condition">满{{coupon.minAmount}}可用</div>
</div>
<div class="coupon-right">
<div class="name">{{coupon.name}}</div>
<div class="date">有效期至 {{coupon.endTime | date:'yyyy.MM.dd'}}</div>
</div>
</div>
</div>
</ion-list>
</div>
</ion-content>
<ion-footer *ngIf="isLoggedIn">
<ion-toolbar>
<div class="logout-button">
<ion-button expand="block" color="danger" (click)="logout()">
退出登录
</ion-button>
</div>
</div>
</ion-content>
</ion-toolbar>
</ion-footer>

@ -8,11 +8,12 @@ ion-content {
}
.login-wrapper {
min-height: 100%;
display: flex;
align-items: flex-start;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
height: 100%;
padding: 32px 16px;
}
.login-container {
@ -106,59 +107,28 @@ ion-segment {
//
.profile-wrapper {
ion-list {
background: transparent;
padding: 0;
margin: 16px 0;
}
}
.profile-header {
--background: var(--card-background);
border-radius: 8px;
margin-bottom: 16px;
ion-avatar {
width: 60px;
height: 60px;
}
h2 {
font-size: 18px;
font-weight: 500;
margin-bottom: 4px;
color: var(--ion-color-dark);
.profile-header {
--padding-start: 16px;
--inner-padding-end: 16px;
--background: transparent;
ion-label {
h2 {
font-size: 16px;
color: #333;
margin: 0;
}
}
}
p {
ion-item-divider {
--background: #f5f5f5;
--color: #999;
font-size: 14px;
color: var(--ion-color-medium);
margin-top: 12px;
}
}
ion-item-divider {
--background: transparent;
--padding-start: 0;
--border-width: 0;
font-size: 16px;
font-weight: 500;
margin: 16px 0 8px;
}
ion-item[button] {
--background: var(--card-background);
border-radius: 8px;
margin-bottom: 8px;
ion-icon {
font-size: 20px;
}
}
.logout-button {
margin-top: 32px;
}
.image-captcha-item {
.image-captcha-wrapper {
display: flex;
@ -187,3 +157,98 @@ ion-item.image-captcha-item {
margin-bottom: 8px;
}
}
.login-wrapper {
.login-content {
text-align: center;
.login-tip {
color: #999;
font-size: 14px;
margin-bottom: 32px;
}
ion-button {
width: 200px;
}
}
}
.coupon-list {
padding: 16px;
border-radius: 100px;
.coupon-item {
display: flex;
background: #fff;
padding: 16px;
margin-bottom: 12px;
.coupon-left {
width: 100px;
display: flex;
flex-direction: column;
.amount {
color: #ff4d4f;
display: flex;
align-items: baseline;
.symbol {
font-size: 14px;
}
.value {
font-size: 24px;
font-weight: normal;
}
}
.condition {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
.coupon-right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.name {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.date {
font-size: 12px;
color: #999;
}
}
}
}
.logout-button {
margin-top: 32px;
padding: 0 16px;
}
ion-footer {
ion-toolbar {
--padding-top: 8px;
--padding-bottom: 8px;
--border-top:none;
// --background: #fff;
}
.logout-button {
padding: 0 16px;
ion-button {
margin: 0;
}
}
}

@ -1,17 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MinePage } from './mine.page';
describe('MinePage', () => {
let component: MinePage;
let fixture: ComponentFixture<MinePage>;
beforeEach(() => {
fixture = TestBed.createComponent(MinePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,9 +1,12 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import {Component, OnInit, OnDestroy, AfterViewInit} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ToastController } from '@ionic/angular';
import {NavController, ToastController,ViewWillEnter, ViewDidEnter, ViewWillLeave, ViewDidLeave } from '@ionic/angular';
import {getUser, MineService} from './mine.service';
import { Router } from '@angular/router';
import {NavigationEnd, Router} from '@angular/router';
import {UserInfo} from "../shared/model/user";
import {Subscription} from "rxjs";
import { HomeService } from '../home/home.service';
import { Coupon } from '../shared/model/coupon';
@Component({
@ -12,7 +15,7 @@ import {UserInfo} from "../shared/model/user";
styleUrls: ['./mine.page.scss'],
standalone:false
})
export class MinePage implements OnInit, OnDestroy {
export class MinePage implements OnInit, OnDestroy {
isLoggedIn = false;
userInfo: UserInfo | null = null;
loginType: 'sms' | 'password' = 'sms';
@ -20,43 +23,53 @@ export class MinePage implements OnInit, OnDestroy {
countdown = 0;
canSendCode = false;
captchaImage: string = '';
private routerSub!: Subscription;
couponCount: number = 0;
coupons: Coupon[] = [];
constructor(
private mineService: MineService,
private fb: FormBuilder,
private router: Router,
private navCtrl: NavController,
private toastCtrl: ToastController,
private homeService: HomeService
) {
getUser().subscribe(res=>{
console.log(res)
if(res.username == "") {
this.isLoggedIn = false;
this.router.navigate(['/mine/login'])
} else {
this.isLoggedIn = true;
this.getUserInfo()
}
})
}
ngOnInit() {
this.checkLoginStatus();
ngOnInit() {
this.routerSub = this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
if(event.url == "/mine") {
this.checkLoginStatus()
}
}
});
}
ngOnDestroy() {
if (this.captchaImage) {
URL.revokeObjectURL(this.captchaImage);
}
if (this.routerSub) {
this.routerSub.unsubscribe();
}
}
checkLoginStatus() {
const token = localStorage.getItem('token');
if (token) {
this.isLoggedIn = true;
this.getUserInfo();
}
getUser().subscribe(res=>{
console.log(res)
if(res.username == "") {
this.isLoggedIn = false;
this.navCtrl.navigateForward("/mine/login")
} else {
this.isLoggedIn = true;
this.getUserInfo()
this.getCouponList()
}
})
}
@ -69,7 +82,7 @@ export class MinePage implements OnInit, OnDestroy {
},
error: (error) => {
this.showToast(error?.message || '获取用户信息失败', 'danger');
this.logout();
// this.logout();
}
});
}
@ -92,7 +105,12 @@ export class MinePage implements OnInit, OnDestroy {
localStorage.removeItem('token');
this.isLoggedIn = false;
this.userInfo = null;
this.coupons = [];
this.showToast('已退出登录', 'success');
localStorage.removeItem('token');
localStorage.removeItem("giftCount")
this.couponCount = 0;
this.router.navigate(['/mine/login']);
}
private showToast(message: string, color: 'success' | 'danger' = 'success') {
@ -104,4 +122,10 @@ export class MinePage implements OnInit, OnDestroy {
cssClass: 'ion-text-center'
}).then(toast => toast.present());
}
getCouponList() {
this.homeService.getCouponList().subscribe(coupons => {
this.coupons = coupons;
});
}
}

@ -35,7 +35,6 @@ export class MineService {
* @returns Observable<void>
*/
sendSmsCaptcha(phone: string, verifyToken: string): Observable<void> {
console.log("发送短信验证码,手机号:", phone);
return this.http.post<void>(`/api/auth/sms-captcha`, {
phone
}, {
@ -70,31 +69,22 @@ export class MineService {
return this.http.get<UserInfo>('/api/user/profile', );
}
verifyImageCaptcha(phone: string, captchaCode: string): Observable<void> {
return this.http.post<void>('/api/auth/verify-image', { phone, captchaCode }, { withCredentials: true });
}
verifySmsCaptcha(phone: string, captchaCode: string): Observable<void> {
return this.http.post<void>('/api/auth/verify-sms', { phone, captchaCode }, { withCredentials: true });
}
sendVerificationCode(phone: string): Observable<void> {
return this.http.post<void>('/api/auth/sms/code', { phone }, { withCredentials: true });
getGuestToken():Observable<string> {
return this.http.get<string>('/api/user/guest')
}
}
export const getLoggedIn = () => {
const token: string | null = sessionStorage.getItem('token');
const token: string | null = localStorage.getItem('token');
const jwtHelper = new JwtHelper();
return of(token != null && !jwtHelper.isTokenExpired(token));
};
export const getUser = (): Observable<UserDetail> => {
const jwtHelper = new JwtHelper();
const token = sessionStorage.getItem('token');
const token = localStorage.getItem('token');
if (token != null) {
const userObj = jwtHelper.decodeToken(token);
console.log(userObj)
return of({
username: userObj.username,
guest_id: userObj.guest_id,
@ -103,3 +93,23 @@ export const getUser = (): Observable<UserDetail> => {
return EMPTY
}
};
export const getGiftCount = ():Observable<number> => {
if(localStorage.getItem("giftCount")){
return of(parseInt(localStorage.getItem("giftCount")!))
} else {
return of(0)
}
}
export const useGiftCount = () => {
if(localStorage.getItem("giftCount")){
let n = parseInt(localStorage.getItem("giftCount")!)
if(n < 1) {
localStorage.setItem("giftCount",0 +"")
} else {
localStorage.setItem("giftCount",n-1 +"")
}
}
}

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

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { PrivacyPageRoutingModule } from './privacy-routing.module';
import { PrivacyPage } from './privacy.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
PrivacyPageRoutingModule
],
declarations: [PrivacyPage]
})
export class PrivacyPageModule {}

@ -0,0 +1,13 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="" defaultHref="/mine"></ion-back-button>
</ion-buttons>
<ion-title>用户隐私政策</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="privacy-content" [innerHTML]="privacyContent">
</div>
</ion-content>

@ -0,0 +1,40 @@
.privacy-content {
padding: 20px;
font-size: 14px;
line-height: 1.6;
color: #333;
h1 {
font-size: 20px;
font-weight: bold;
margin: 20px 0 15px;
}
h2 {
font-size: 18px;
font-weight: bold;
margin: 15px 0 10px;
}
h3 {
font-size: 16px;
font-weight: bold;
margin: 12px 0 8px;
}
p {
margin: 8px 0;
}
strong {
font-weight: bold;
}
em {
font-style: italic;
}
}
ion-back-button {
--color: #333333;
}

@ -0,0 +1,40 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-privacy',
templateUrl: './privacy.page.html',
styleUrls: ['./privacy.page.scss'],
standalone:false,
})
export class PrivacyPage implements OnInit {
privacyContent: SafeHtml = '';
constructor(
private http: HttpClient,
private sanitizer: DomSanitizer
) { }
ngOnInit() {
this.loadPrivacyContent();
}
private loadPrivacyContent() {
this.http.get('assets/privacy/readme.md', { responseType: 'text' })
.subscribe(content => {
const htmlContent = this.convertMarkdownToHtml(content);
this.privacyContent = this.sanitizer.bypassSecurityTrustHtml(htmlContent);
});
}
private convertMarkdownToHtml(markdown: string): string {
return markdown
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>');
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 225 KiB

@ -0,0 +1,70 @@
## 用户协议
欢迎您使用上海路诚数据服务有限公司(以下简称“本公司”)提供的财经资讯服务。在使用本服务之前,请您仔细阅读本《用户协议》(以下简称“本协议”)。
### 一、服务内容
本公司通过H5页面向用户提供与财联社类似的财经资讯服务包括但不限于新闻资讯、市场数据、分析报告等。
### 二、用户注册与账户管理
1.用户需通过手机号或其他方式注册账号,并对所提供的信息真实性负责。
2.用户应妥善保管账户信息,不得将账户转让、出借或共享给他人使用。
3.若发现账户存在异常使用情况,应立即通知本公司,否则由此产生的责任由用户自行承担。
### 三、用户权利与义务
1.用户有权在遵守本协议的前提下,使用本公司提供的财经资讯服务。
2.用户不得利用本服务从事任何违法、违规或侵犯他人合法权益的行为。
3.用户不得未经授权转载、复制、出售或用于商业用途,否则本公司有权追究其责任。
### 四、免责声明
1.本公司提供的财经资讯仅供参考,不构成投资建议,用户据此操作产生的风险由用户自行承担。
2.本公司不对数据的完整性、准确性、可靠性做任何承诺。
3.因不可抗力、网络故障或第三方原因导致的服务中断,本公司不承担责任。
### 五、协议变更与终止
1.本公司有权根据业务需要调整本协议内容变更后的协议将在H5页面公示用户继续使用服务即表示接受变更。
2.用户若违反本协议,本公司有权终止其服务并注销账户。
### 六、法律适用与争议解决
本协议适用中华人民共和国法律,因本协议引发的争议,由本公司所在地法院管辖。
## 隐私政策
本公司重视用户的隐私保护,在使用本服务时,您同意本《隐私政策》。
### 一、信息收集
1.本公司可能收集用户在注册、使用服务过程中提供的信息,包括但不限于手机号、设备信息、访问日志等。
2.本公司可能通过Cookies等技术收集用户使用情况数据以优化服务体验。
### 二、信息使用
1.本公司收集的信息将用于提供、优化和改进服务,包括但不限于用户身份验证、数据统计分析等。
2.本公司不会向任何第三方出售或披露用户个人信息,除非获得用户授权或法律法规要求。
### 三、信息存储与安全
1.本公司采取合理的安全措施保护用户信息,防止数据泄露、篡改或丢失。
2.若发生数据泄露事件,本公司将在法律规定的范围内采取补救措施。
### 四、用户权利
1.用户可以查询、更正或删除个人信息,并有权撤回同意。
2.用户可通过联系客服注销账户,注销后信息将按照法律规定处理。
### 五、政策更新
本公司有权对本隐私政策进行调整更新后的政策将在H5页面公示继续使用服务即表示接受变更。
### 六、联系方式
如对本政策有任何疑问,可通过以下方式联系我们:
联系邮箱qiuxing@lcsuo.cn
联系电话18328523903
公司地址上海市宝山区地杰路58号1幢1层
本协议与政策最终解释权归上海路诚数据服务有限公司所有。

File diff suppressed because it is too large Load Diff

@ -183,13 +183,13 @@ func NewPasswordEncoder() security.PasswordEncoder {
return &security.BCryptPasswordEncoder{}
}
func NewSmsService(client *Ihttp.Client) sms.IsmsService {
return sms.NewDebugSmsService(client)
func NewSmsService() sms.IsmsService {
return sms.NewSmsService()
}
func NewInternalClient() *Ihttp.Client {
return Ihttp.NewClient()
}
func NewJWTAuthMiddleware(appConfig *AppConfig) *auth_middleware.AuthMiddleware {
return auth_middleware.NewAuthMiddleware(appConfig.JwtConfig.Secret, nil)
func NewJWTAuthMiddleware(appConfig *AppConfig, log logger.New, redis redis.Cmdable) *auth_middleware.AuthMiddleware {
return auth_middleware.NewAuthMiddleware(appConfig.JwtConfig.Secret, nil, redis, log)
}

@ -29,4 +29,6 @@ redis:
addr: 123.206.181.43:16379
password: Admin999
db: 0
sms:
api: sms.tencentcloudapi.com
url:

@ -66,6 +66,8 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1135 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.1115 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/otel v1.11.2 // indirect

@ -563,6 +563,11 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1115/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1135 h1:NAu4sH5c+kGTZQ0rwhnuYjIXbentw3Np+TbwimH22uc=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1135/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.1115 h1:LINYuPsgE4yVgeakbliQQPuiSGKI8H4zNOwfUMIbxvw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.1115/go.mod h1:NLUwEcjDCXtAQSNLBdm2yU4M+zVRn71NCsS/G8l5eZc=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=

@ -7,6 +7,7 @@ import (
domainUser "cls/internal/domain/user"
"cls/pkg/logger"
"cls/pkg/util/page"
"errors"
"fmt"
"strings"
"time"
@ -19,7 +20,6 @@ type ArticleService struct {
purchaseRepo purchase.Repository
userAggregateRepo domainUser.UserAggregateRepository
FreeRepo free_trial.FreeTrialRepository
resp *ArticleResp
log logger.Logger
}
@ -28,14 +28,12 @@ func NewArticleService(repo article.ArticleRepository,
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")}
}
@ -101,8 +99,8 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s
articleIds = append(articleIds, v.Id)
}
}
articleIds = nil
purchaseData, err := a.purchaseRepo.FindArticleById(user.Id, articleIds...)
articleIds = nil
if err != nil {
a.log.Error(err.Error())
}
@ -113,6 +111,7 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s
}
articleIds = nil
}
result := make([]*ArticleDto, 0, len(articles))
@ -120,7 +119,7 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s
t := time.Unix(v.Ctime, 0) // 秒数和纳秒数0 表示没有纳秒部分
m, g := a.getMainGrowthBoard(v.Stocks)
_, lock := purchaseId[v.Id]
result = append(result, &ArticleDto{
ad := &ArticleDto{
EventId: v.Id,
Title: v.Title,
Class: class_type[v.Type],
@ -130,7 +129,13 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s
MainBoard: m,
GrowthBoard: g,
Unlock: lock,
})
}
if lock {
ad.Stocks = v.Stocks
ad.Content = v.Content
}
result = append(result, ad)
}
purchaseId = nil
articles = nil
@ -146,7 +151,7 @@ func (a *ArticleService) FindUnLock(ePhone string, page *page.Page, searchParams
return err
}
conds := make([]builder.Cond, 0)
class := searchParams["search_like_class"]
class := searchParams["search_eq_class"]
if class != "" {
classCode := class_type_reverse[class]
if classCode != 0 {
@ -166,7 +171,7 @@ func (a *ArticleService) FindUnLock(ePhone string, page *page.Page, searchParams
for _, v := range purchaseData {
pId = append(pId, v.ContentId)
}
conds = append(conds, builder.In("id"))
conds = append(conds, builder.In("id", pId))
articles := make([]*article.LianV1Article, 0)
page.Content = &articles
err = a.repo.Find(page, conds)
@ -189,7 +194,7 @@ func (a *ArticleService) FindUnLock(ePhone string, page *page.Page, searchParams
Stocks: v.Stocks,
MainBoard: m,
GrowthBoard: g,
Unlock: false,
Unlock: true,
})
}
articles = nil
@ -285,6 +290,11 @@ func (a *ArticleService) UnLockArticle(ePhone string, aid uint64) (*ArticleDto,
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
u := userAggregate.GetUser()
if u.GiftCount < 1 {
a.log.Errorf("用户【%d】免费次数不够", u.Id)
return nil, errors.New("没有次数了")
}
// 2. 获取文章
article, err := a.repo.GetArticleById(aid)
if err != nil {

@ -13,9 +13,7 @@ import (
images "github.com/wenlng/go-captcha-assets/resources/images_v2"
"github.com/wenlng/go-captcha-assets/resources/tiles"
"github.com/wenlng/go-captcha/v2/slide"
"log"
"math/rand"
"strings"
"sync"
"time"
)
@ -97,20 +95,19 @@ func NewCaptchaService(rdb redis.Cmdable, captcha *captchafx.Captcha, log logger
// GenerateSlideVerify 生成滑动验证码
func (c *CaptchaService) GenerateSlideVerify(phone string) (*SlideVerifyResp, error) {
builder := slide.NewBuilder(
// slide.WithGenGraphNumber(2),
// slide.WithEnableGraphVerticalRandom(true),
)
builder := slide.NewBuilder()
// background images
imgs, err := images.GetImages()
if err != nil {
log.Fatalln(err)
c.log.Error(err)
return nil, err
}
graphs, err := tiles.GetTiles()
if err != nil {
log.Fatalln(err)
c.log.Error(err)
return nil, err
}
var newGraphs = make([]*slide.GraphImage, 0, len(graphs))
@ -132,25 +129,26 @@ func (c *CaptchaService) GenerateSlideVerify(phone string) (*SlideVerifyResp, er
slideCapt := builder.Make()
captData, err := slideCapt.Generate()
if err != nil {
log.Fatalln(err)
c.log.Error(err)
return nil, err
}
dotData := captData.GetData()
if dotData == nil {
log.Fatalln(">>>>> generate err")
c.log.Error("生成滑动验证码数据失败")
return nil, errors.New("生成失败")
}
dots, _ := json.Marshal(dotData)
fmt.Println(">>>>> ", string(dots))
var mBase64, tBase64 string
mBase64, err = captData.GetMasterImage().ToBase64()
if err != nil {
fmt.Println(err)
c.log.Error(err)
return nil, err
}
tBase64, err = captData.GetTileImage().ToBase64()
if err != nil {
fmt.Println(err)
c.log.Error(err)
return nil, err
}
// 生成验证token
@ -165,7 +163,7 @@ func (c *CaptchaService) GenerateSlideVerify(phone string) (*SlideVerifyResp, er
}
verifyData, _ := json.Marshal(verifyInfo)
err = c.rdb.Set(context.Background(), token, string(verifyData), 5*time.Minute).Err()
err = c.rdb.Set(context.Background(), token, string(verifyData), 1*time.Minute).Err()
if err != nil {
c.log.Error("保存验证信息失败:", err)
return nil, errors.New("生成验证码失败")
@ -251,9 +249,8 @@ func (c *CaptchaService) GenerateSmsCaptcha(username string, phone string) (*Sms
// 3. 生成验证码
code := c.generateSmsCode()
// 4. 存储验证码
key := c.getSmsVerifyKey(username, phone)
key := c.getSmsVerifyKey(phone)
err = c.rdb.Set(context.Background(), key, code, 5*time.Minute).Err()
if err != nil {
c.log.Error("存储验证码失败:", err)
@ -285,7 +282,7 @@ func (c *CaptchaService) VerifySmsCaptcha(username, phone, captchaCode string) e
}
// 1. 获取存储的验证码
key := c.getSmsVerifyKey(username, phone)
key := c.getSmsVerifyKey(phone)
fmt.Println("校验key", key)
storedCode, err := c.rdb.Get(context.Background(), key).Result()
if err == redis.Nil {
@ -311,12 +308,8 @@ func (c *CaptchaService) VerifySmsCaptcha(username, phone, captchaCode string) e
}
// getSmsVerifyKey 生成短信验证码的Redis键
func (c *CaptchaService) getSmsVerifyKey(username, phone string) string {
fmt.Println("username====>", username)
if strings.HasPrefix(username, "G") {
return fmt.Sprintf("guest:sms-captcha:%s:%s", username, phone)
}
return fmt.Sprintf("user:sms-captcha:%s:%s", username, phone)
func (c *CaptchaService) getSmsVerifyKey(phone string) string {
return fmt.Sprintf("sms-captcha:%s", phone)
}
// checkLimit 检查频率限制

@ -8,6 +8,7 @@ import (
"cls/pkg/util/page"
"cls/pkg/web"
"errors"
"time"
)
var (
@ -50,17 +51,34 @@ func (s *Service) CreateColumn(req *CreateColumnReq) (*ColumnDto, error) {
// GetColumn 获取专栏信息
func (s *Service) GetColumn(ePhone string, name string) (*ColumnDto, error) {
unlock := false
col, err := s.repo.FindByName(name)
if err != nil {
s.log.Error("failed to find column", "error", err)
return nil, err
}
if ePhone != "" {
u, _ := s.userRepo.FindByPhone(ePhone)
if u != nil {
data, _ := s.purchaseRepo.FindColumnWithId(u.Id, col.ID)
if data != nil {
if data.ContentId == col.ID {
unlock = true
}
}
}
}
columnDto := &ColumnDto{
ID: col.ID,
Title: col.Title,
Brief: col.Brief,
Cover: col.Brief,
Unlock: false,
ID: col.ID,
Title: col.Title,
Brief: col.Brief,
Cover: col.Brief,
ArticleNum: col.ArticleNum,
FollowNum: col.FollowNum,
PurchaseNum: col.PurchaseNum,
Unlock: unlock,
CreatedAt: time.Time{},
}
if ePhone == "" {
return columnDto, nil

@ -2,10 +2,10 @@ package price
import (
"cls/internal/domain/price"
"cls/internal/domain/price_default"
"cls/pkg/logger"
"cls/pkg/util/page"
"cls/pkg/web"
"errors"
"fmt"
)
var (
@ -16,42 +16,20 @@ var (
// PriceService 价格管理服务
type PriceService struct {
repo price.PriceRepository
log logger.Logger
repo price.PriceRepository
pdRepo price_default.PriceDefaultRepository
log logger.Logger
}
// NewService 创建价格管理服务
func NewPriceService(repo price.PriceRepository, log logger.New) *PriceService {
func NewPriceService(repo price.PriceRepository, pdRepo price_default.PriceDefaultRepository, log logger.New) *PriceService {
return &PriceService{
repo: repo,
log: log("cls:service:price"),
repo: repo,
pdRepo: pdRepo,
log: log("cls:service:price"),
}
}
// SetPrice 设置价格
func (s *PriceService) SetPrice(dto *PriceDto) error {
if dto.Amount < 0 {
return ErrInvalidAmount
}
// 检查是否已存在价格记录
existingPrice, err := s.repo.FindByTargetID(dto.TargetID, dto.Type)
if err != nil {
// 如果记录不存在,创建新记录
newPrice := dto.ToPrice()
return s.repo.Save(newPrice)
}
// 如果记录存在,更新价格
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)
@ -64,37 +42,49 @@ func (s *PriceService) GetColumnPrice(dto *PriceDto) (*PriceDto, error) {
// GetPrice 获取价格(如果不存在则使用默认价格)
func (s *PriceService) GetPrice(dto *PriceDto) (*PriceDto, error) {
fmt.Println("=================")
fmt.Println("GetPrice")
fmt.Println(dto)
// 检查是否已存在价格记录
existingPrice, err := s.repo.FindByTargetID(dto.TargetID, dto.Type)
if err != nil {
// 如果记录不存在,使用默认价格
var defaultAmount int64
switch dto.Type {
case price.TypeArticle:
defaultAmount = price.DefaultArticlePrice
case price.TypeColumn:
defaultAmount = price.DefaultColumnPrice
default:
return nil, ErrInvalidType
}
// 创建默认价格记录
newPrice := price.NewPrice(dto.TargetID, dto.Type, defaultAmount, 0)
if err := s.repo.Save(newPrice); err != nil {
s.log.Error(err)
return nil, err
}
fmt.Println(existingPrice)
if existingPrice == nil { //价格不存在
fmt.Println("no have data")
//get default price
dp, err := s.pdRepo.Get()
if err != nil {
s.log.Error(err.Error())
return nil, err
}
fmt.Println("default price")
fmt.Println(dp)
existingPrice = &price.Price{
TargetID: dto.TargetID,
Type: dto.Type,
Discount: dp.Discount,
}
if dto.Type == price.TypeArticle {
return &PriceDto{
Amount: newPrice.Amount,
Discount: newPrice.Discount,
}, nil
existingPrice.Amount = dp.Amount
} else {
existingPrice.OneMonthPrice = dp.OneMonthPrice
existingPrice.ThreeMonthsPrice = dp.ThreeMonthsPrice
existingPrice.SixMonthsPrice = dp.SixMonthsPrice
existingPrice.OneYearPrice = dp.OneYearPrice
existingPrice.FirstMontDiscount = dp.FirstMontDiscount
}
return &PriceDto{
OneMonthPrice: newPrice.OneMonthPrice,
ThreeMonthsPrice: newPrice.ThreeMonthsPrice,
SixMonthsPrice: newPrice.SixMonthsPrice,
OneYearPrice: newPrice.OneYearPrice,
Discount: 0.3,
}, nil
fmt.Println("insert")
err = s.repo.Save(existingPrice)
if err != nil {
s.log.Error(err)
return nil, err
}
fmt.Println("inset done")
}
priceDto := &PriceDto{
Discount: existingPrice.Discount,
@ -108,33 +98,7 @@ func (s *PriceService) GetPrice(dto *PriceDto) (*PriceDto, error) {
priceDto.OneYearPrice = existingPrice.OneYearPrice
}
return priceDto, nil
}
fmt.Println(priceDto)
// GetPriceList 获取价格列表
func (s *PriceService) GetPriceList(page *page.Page, params map[string]string) error {
conds := web.ParseFilters(params)
return s.repo.FindAll(page, conds)
}
// DeletePrice 删除价格记录
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)
return priceDto, nil
}

@ -22,21 +22,21 @@ const (
// 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"` // 删除时间
ID uint64 `xorm:"pk autoincr"` // 优惠券ID
Code string `xorm:"varchar(255)"` // 优惠券码
Name string `xorm:"varchar(255)"` // 优惠券名称
Type CouponType `xorm:"int"` // 优惠券类型
Value int64 `xorm:"int"` // 优惠券值
MinAmount int64 `xorm:"int"` // 最低使用金额
StartTime time.Time `xorm:"datetime"` // 开始时间
EndTime time.Time `xorm:"datetime"` // 结束时间
Status CouponStatus `xorm:"int"` // 优惠券状态
AdminID uint64 `xorm:"int"` // 创建者ID
UserID uint64 `xorm:"int"` // 使用者ID
UsedAt time.Time `xorm:"datetime"` // 使用时间
CreatedAt time.Time `xorm:"created"` // 创建时间
UpdatedAt time.Time `xorm:"updated"` // 更新时间
DeletedAt time.Time `xorm:"deleted"` // 删除时间
}
// TableName 指定表名

@ -12,19 +12,20 @@ const (
// Price 价格管理实体
type Price struct {
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'"`
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年价格
FirstMontDiscount float32 `xorm:"not null 'first_mont_discount'"` // 首月折扣
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 默认价格(分)

@ -0,0 +1,12 @@
package price_default
type PriceDefault struct {
Id uint64 `xorm:"pk autoincr 'id'"`
Amount int64 `xorm:"not null 'amount'"` // 单篇文章价格(分)
FirstMontDiscount float32 `xorm:"null"` //首月优惠折扣
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"`
}

@ -0,0 +1,5 @@
package price_default
type PriceDefaultRepository interface {
Get() (*PriceDefault, error)
}

@ -15,4 +15,5 @@ type Repository interface {
// FindByContent 查找内容的所有购买记录
FindByContent(contentId uint64, contentType ContentType) ([]*Purchase, error)
FindColumnWithId(uid uint64, contentId uint64) (*Purchase, error)
}

@ -18,6 +18,7 @@ func DefaultConfig() *Config {
"/api/auth/verify-image",
"/api/auth/verify-sms",
"/api/article/all",
"/api/user/guest",
},
TokenKey: "Authorization",
}

@ -1,9 +1,13 @@
package auth
import (
"cls/pkg/logger"
"context"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"github.com/redis/go-redis/v9"
"time"
)
@ -22,16 +26,20 @@ type Claims struct {
type AuthMiddleware struct {
config *Config
secretKey []byte
log logger.Logger
redis redis.Cmdable
}
// NewAuthMiddleware 创建认证中间件
func NewAuthMiddleware(secretKey string, config *Config) *AuthMiddleware {
func NewAuthMiddleware(secretKey string, config *Config, redis redis.Cmdable, log logger.New) *AuthMiddleware {
if config == nil {
config = DefaultConfig()
}
return &AuthMiddleware{
config: config,
secretKey: []byte(secretKey),
log: log("cls:infrastructure:middleware"),
redis: redis,
}
}
@ -39,23 +47,16 @@ func NewAuthMiddleware(secretKey string, config *Config) *AuthMiddleware {
func (m *AuthMiddleware) Handle() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
fmt.Printf("处理请求: %s\n", path)
// 获取token
token := m.ExtractToken(c)
fmt.Printf("获取到的token: %s\n", token)
// 如果没有token创建临时游客token
if token == "" {
fmt.Println("没有token创建临时游客token")
newToken, guestId, err := m.generateGuestToken()
if err != nil {
fmt.Printf("生成临时token失败: %v\n", err)
c.JSON(500, gin.H{"error": "生成临时token失败"})
c.Abort()
return
}
fmt.Printf("生成的游客ID: %s\n", guestId)
// 设置token到响应头
c.Header(m.config.TokenKey, "Bearer "+newToken)
@ -69,7 +70,6 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc {
// 如果是跳过认证的路径,直接继续
if m.shouldSkip(path) {
fmt.Printf("跳过认证: %s\n", path)
c.Next()
return
}
@ -81,16 +81,13 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc {
// 验证已有token
claims, err := m.validateToken(token)
if err != nil {
fmt.Printf("token验证失败: %v\n", err)
// token无效创建新的临时游客token
newToken, guestId, err := m.generateGuestToken()
if err != nil {
fmt.Printf("生成临时token失败: %v\n", err)
c.JSON(500, gin.H{"error": "生成临时token失败"})
c.Abort()
return
}
fmt.Printf("生成的游客ID: %s\n", guestId)
c.Header(m.config.TokenKey, "Bearer "+newToken)
// 设置 claims 到上下文
@ -103,7 +100,6 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc {
// 如果是跳过认证的路径,直接继续
if m.shouldSkip(path) {
fmt.Printf("跳过认证: %s\n", path)
c.Next()
return
}
@ -112,18 +108,15 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc {
return
}
fmt.Printf("token验证成功claims: %+v\n", claims)
// token有效根据phone判断是否为游客
isGuest := claims.Username == ""
// 设置 claims 到上下文
mapClaims := map[string]interface{}{}
if isGuest {
fmt.Printf("设置游客ID: %s\n", claims.GuestId)
mapClaims["guest_id"] = claims.GuestId
c.Set("guest_id", claims.GuestId)
} else {
fmt.Printf("设置用户手机号: %s\n", claims.Username)
mapClaims["username"] = claims.Username
c.Set("username", claims.Username)
}
@ -132,7 +125,6 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc {
// 如果是跳过认证的路径,直接继续
if m.shouldSkip(path) {
fmt.Printf("跳过认证: %s\n", path)
c.Next()
return
}
@ -167,7 +159,6 @@ func (m *AuthMiddleware) generateGuestToken() (string, string, error) {
// GenerateUserToken 生成用户token
func (m *AuthMiddleware) GenerateUserToken(encryptedPhone string) (string, error) {
fmt.Println("===GenerateUserToken=>>>>>>>>>", encryptedPhone)
claims := &Claims{
Username: encryptedPhone,
GuestId: "", // 登录用户不需要游客ID
@ -182,7 +173,16 @@ func (m *AuthMiddleware) GenerateUserToken(encryptedPhone string) (string, error
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(m.secretKey)
tokenStr, err := token.SignedString(m.secretKey)
if err != nil {
return "", err
}
err = m.redis.Set(context.Background(), encryptedPhone, tokenStr, time.Now().Add(30*24*time.Hour).Sub(time.Now())).Err()
if err != nil {
return "", err
}
return tokenStr, nil
}
// validateToken 验证token
@ -198,17 +198,23 @@ func (m *AuthMiddleware) validateToken(tokenString string) (*Claims, error) {
return nil, fmt.Errorf("解析token失败: %v", err)
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
claims, ok := token.Claims.(*Claims)
if ok && token.Valid {
if claims.Username != "" {
result, _ := m.redis.Get(context.Background(), claims.Username).Result()
if result != tokenString {
return nil, errors.New("用户token失效")
}
}
return claims, nil
}
return nil, fmt.Errorf("无效的token")
}
// extractToken 从请求头中提取token
func (m *AuthMiddleware) ExtractToken(c *gin.Context) string {
auth := c.GetHeader(m.config.TokenKey)
fmt.Printf("Authorization头: %s\n", auth)
if auth == "" {
return ""
@ -216,7 +222,6 @@ func (m *AuthMiddleware) ExtractToken(c *gin.Context) string {
const prefix = "Bearer "
if len(auth) <= len(prefix) || auth[:len(prefix)] != prefix {
fmt.Println("token格式不正确缺少Bearer前缀")
return ""
}

@ -33,7 +33,7 @@ func (a ArticleRepositoryORM) Find(page *page.Page, conds []builder.Cond) error
func (a ArticleRepositoryORM) GetArticleById(id uint64) (*article.LianV1Article, error) {
article := &article.LianV1Article{}
has, err := a.engine.Where(builder.Eq{"id": id}).Get(article)
has, err := a.engine.Cls.Where(builder.Eq{"id": id}).Get(article)
if err != nil {
a.log.Error(err)
return nil, err

@ -61,8 +61,7 @@ func (p *PriceRepositoryORM) FindByTargetID(targetID uint64, priceType price.Pri
return nil, err
}
if !has {
p.log.Errorf("未找到相关数据【target_id: %d, type: %d】", targetID, priceType)
return nil, errors.New("记录不存在")
return nil, nil
}
return price, nil
}

@ -0,0 +1,27 @@
package price_default
import (
"cls/internal/domain/price_default"
"cls/pkg/xorm_engine"
"xorm.io/builder"
)
type PriceDefaultRepositoryORM struct {
engine *xorm_engine.Engine
}
var _ price_default.PriceDefaultRepository = (*PriceDefaultRepositoryORM)(nil)
func NewPriceRepositoryORM(engine *xorm_engine.Engine) price_default.PriceDefaultRepository {
return &PriceDefaultRepositoryORM{engine}
}
func (p PriceDefaultRepositoryORM) Get() (*price_default.PriceDefault, error) {
data := &price_default.PriceDefault{}
_, err := p.engine.Where(builder.Eq{"id": 1}).Get(data)
if err != nil {
return nil, err
}
return data, err
}

@ -78,3 +78,12 @@ func (r *PurchaseRepositoryORM) FindColumnById(uid uint64, ids ...uint64) ([]*pu
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
}
func (r *PurchaseRepositoryORM) FindColumnWithId(uid uint64, contentId uint64) (*purchase.Purchase, error) {
data := &purchase.Purchase{}
_, err := r.engine.Where("user_id = ? AND content_id = ? AND content_type = ?", uid, contentId, purchase.ContentTypeColumn).Get(data)
if err != nil {
return nil, err
}
return data, nil
}

@ -58,26 +58,21 @@ func (u *UserAggregateRepositoryORM) SaveUserAggregate(aggregate *domainUser.Use
u.log.Error("开启事务失败", err)
return err
}
defer func() {
if err := recover(); err != nil {
session.Rollback()
panic(err)
}
}()
defer session.Close()
// 2. 更新用户信息
if _, err := session.Where(builder.Eq{"id": aggregate.GetUser().Id}).Update(aggregate.GetUser()); err != nil {
session.Rollback()
if _, err := session.Where(builder.Eq{"id": aggregate.GetUser().Id}).Cols("gift_count").Unscoped().Update(aggregate.GetUser()); err != nil {
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
if p.Id < 1 {
if _, err := session.Insert(p); err != nil {
u.log.Error("保存购买记录失败", err)
return err
}
}
}

@ -25,14 +25,13 @@ func NewArticleHandle(service *article.ArticleService, authMiddleware *middlewar
}
func (a *ArticleHandler) RegisterRouters(router gin.IRouter) {
fmt.Println("register router")
fmt.Printf("%+v\n", a)
articleHandler := router.Group("/article")
{
articleHandler.GET("/all", a.findAll)
articleHandler.GET("/unlock", a.unlock)
articleHandler.GET("/free", a.free)
articleHandler.GET("/detail/:id", a.detail)
articleHandler.POST("/unlock-article", a.unlockArticle)
}
}
func (a *ArticleHandler) findAll(c *gin.Context) {
@ -74,8 +73,7 @@ func (a *ArticleHandler) unlock(c *gin.Context) {
return
}
err = a.service.FindUnLock(ePhone, p, map[string]string{
"search_like_class": c.Query("search_like_class"),
"search_like_unlock": "1"})
"search_eq_class": c.Query("search_eq_class")})
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
} else {
@ -103,19 +101,21 @@ func (a *ArticleHandler) free(c *gin.Context) {
}
func (a *ArticleHandler) unlockArticle(c *gin.Context) {
aid, err := strconv.ParseInt(c.Param("aid"), 10, 64)
dto := &article.ArticleDto{}
err := c.ShouldBindJSON(dto)
if err != nil {
a.log.Error(err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
fmt.Printf("%+v\n", dto)
ePhone, err := a.authMiddleware.DecodeToken(c)
if err != nil {
a.log.Error(err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
data, err := a.service.UnLockArticle(ePhone, uint64(aid))
data, err := a.service.UnLockArticle(ePhone, dto.EventId)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
} else {

@ -4,7 +4,6 @@ import (
"cls/internal/application/price"
"cls/internal/interfaces"
"cls/pkg/logger"
"cls/pkg/util/page"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
@ -24,85 +23,11 @@ func NewPriceHandler(service *price.PriceService, log logger.New) *PriceHandler
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 {

@ -2,6 +2,8 @@ package modules
import (
service "cls/internal/application/price"
"cls/internal/infrastructure/persistence/price_default"
repo "cls/internal/infrastructure/persistence/price"
"cls/internal/interfaces"
"cls/internal/interfaces/price"
@ -13,4 +15,5 @@ var PriceModule = fx.Module("PriceModule",
interfaces.AsHandler(price.NewPriceHandler),
service.NewPriceService,
repo.NewPriceRepositoryORM,
price_default.NewPriceRepositoryORM,
))

@ -9,7 +9,7 @@ import (
func main() {
app := cmd.App()
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "cmcc-panoramic-application: %s\n", err)
fmt.Fprintf(os.Stderr, "cls-H5: %s\n", err)
os.Exit(1)
}
}

@ -2,6 +2,7 @@ package http
import (
"errors"
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/spf13/viper"
"go.uber.org/zap"
@ -73,6 +74,7 @@ func (c *Client) getResponse(method, path string, header http.Header, body io.Re
}
func (c *Client) getParsedResponse(method, path string, header http.Header, body io.Reader, obj interface{}) error {
fmt.Println("start parse response")
data, err := c.getResponse(method, path, header, body)
if err != nil {
return err

@ -2,6 +2,8 @@ package http
import (
"bytes"
"errors"
"fmt"
)
const (
@ -14,16 +16,59 @@ type MsgRes struct {
}
type MsgReq struct {
Code string `json:"code"`
Msg string `json:"msg"`
Code string `json:"code"`
Phone string `json:"msg"`
}
func (c *Client) SubmitPhoneCodeRequest(req *MsgReq) (*MsgRes, error) {
type SmsCaptchaReqForTencent struct {
Action string `json:"Action"`
Version string `json:"Version"`
Region string `json:"Region"`
PhoneNumberSet []string `json:"PhoneNumberSet"`
SmsSdkAppId string `json:"SmsSdkAppId"`
TemplateId string `json:"TemplateId"`
SignName string `json:"SignName"`
TemplateParamSet []string `json:"TemplateParamSet"`
}
type SmsCaptchaResForTencent struct {
SendStatusSet []*SmsCaptchaResForTencentSub `json:"SendStatusSet"`
}
type SmsCaptchaResForTencentSub struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
func (c *Client) SubmitPhoneCodeRequest(phoneMsg *MsgReq) (*SmsCaptchaResForTencentSub, error) {
fmt.Sprintln("=============")
fmt.Println("start send captcha")
req := &SmsCaptchaReqForTencent{
Action: "SendSms",
Version: "2021-01-11",
Region: "ap-guangzhou",
PhoneNumberSet: []string{"+86" + phoneMsg.Phone},
SmsSdkAppId: "1400237962",
TemplateId: "387221",
SignName: "627395",
TemplateParamSet: []string{phoneMsg.Code, "3"},
}
fmt.Println(req)
body, err := json2.Marshal(req)
if err != nil {
fmt.Println(err.Error())
return nil, err
}
resp := &MsgRes{}
logger.Info("request " + smsSendUrl + "\t:" + string(body))
return resp, c.getParsedResponse("POST", smsSendUrl, jsonHeader, bytes.NewReader(body), resp)
fmt.Println(string(body))
resp := &SmsCaptchaResForTencent{}
fmt.Println("gogogo")
err = c.getParsedResponse("POST", smsSendUrl, jsonHeader, bytes.NewReader(body), resp)
if err != nil {
fmt.Println(err.Error())
return nil, err
}
if len(resp.SendStatusSet) == 0 {
return nil, errors.New("发送失败")
}
return resp.SendStatusSet[0], nil
}

@ -1,8 +1,12 @@
package sms
import (
"cls/pkg/http"
"errors"
"fmt"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
ter "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
)
type IsmsService interface {
@ -11,23 +15,47 @@ type IsmsService interface {
// ConsoleSmsService debug环境使用
type SmsService struct {
client *http.Client
client *sms.Client
}
func NewSmsService() IsmsService {
credential := common.NewCredential(
"AKIDa3HEyTihPs40f7JFvLVP3RcXCbkdFbhf",
"oMefPt6pMiofGaigPuAByWq23pMejZBX",
)
// 实例化一个client选项可选的没有特殊需求可以跳过
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := sms.NewClient(credential, "ap-guangzhou", cpf)
return &SmsService{client: client}
}
func (s *SmsService) Send(code string, phone string) error {
fmt.Printf("手机号【%s】验证码【%s】\n", phone, code)
return nil
resp, err := s.client.SubmitPhoneCodeRequest(&http.MsgReq{})
//fmt.Printf("手机号【%s】验证码【%s】\n", phone, code)
//return nil
request := sms.NewSendSmsRequest()
request.PhoneNumberSet = common.StringPtrs([]string{"+86" + phone})
request.SmsSdkAppId = common.StringPtr("1400237962")
request.TemplateId = common.StringPtr("387221")
request.SignName = common.StringPtr("上海路诚数据服务")
request.TemplateParamSet = common.StringPtrs([]string{code, "3"})
response, err := s.client.SendSms(request)
if _, ok := err.(*ter.TencentCloudSDKError); ok {
return err
}
if err != nil {
return err
}
if resp.State == 200 {
return nil
if len(response.Response.SendStatusSet) == 0 {
return errors.New("发送失败")
}
if *response.Response.SendStatusSet[0].Code != "Ok" {
return errors.New(fmt.Sprintf("发送失败[%s]", response.Response.SendStatusSet[0].Message))
}
//fmt.Printf("【中国移动】 您的验证码为:%s您正在进行身份验证该验证码10分钟内有效如非本人操作请忽略本短信\n", msg)
return nil
}
func NewDebugSmsService(client *http.Client) IsmsService {
return &SmsService{client}
}

Loading…
Cancel
Save