Skip to content

Commit

Permalink
Merge pull request #261 from Roger13579/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
y0000ga authored Jun 22, 2024
2 parents 9044923 + 593672f commit 6c2a534
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 38 deletions.
4 changes: 3 additions & 1 deletion src/dto/order/createOrderDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export class CreateOrderDto {
const { price, items, paymentMethod, deliveryInfo } = body;
this.userId = new Types.ObjectId((user as IUser).id);
this.price = price;
this._items = Object.values(groupBy(items, 'productId')).map((group) => ({
const createKey = (item: IOrderItem) =>
`${item.productId}-${item.plan.name}-${item.plan.discount}-${item.plan.headCount}`;
this._items = Object.values(groupBy(items, createKey)).map((group) => ({
productId: group[0].productId,
amount: sumBy(group, 'amount'),
plan: group[0].plan,
Expand Down
1 change: 1 addition & 0 deletions src/models/baseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ModelName {
order = 'Order',
chat = 'Chat',
notify = 'Notify',
shareCode = 'ShareCode',
}

export interface BaseModel extends Document, ITimestamp {
Expand Down
34 changes: 34 additions & 0 deletions src/models/shareCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Schema, model } from 'mongoose';
import { schemaOption } from '../utils/constants';
import { BaseModel, ModelName, schemaDef } from './baseModel';
import { IUserId } from '../types/user.type';
import { ITicketId } from '../types/ticket.type';

const { ticketId } = schemaDef;

export interface IShareCode extends BaseModel, IUserId, ITicketId {
shareCode: string;
expiredAt: Date;
isUsed: boolean;
}

const schema = new Schema<IShareCode>(
{
ticketId,
expiredAt: {
type: Date,
required: true,
},
shareCode: {
type: String,
required: true,
},
isUsed: {
type: Boolean,
required: true,
},
},
{ ...schemaOption },
);

export const ShareCodeModel = model<IShareCode>(ModelName.shareCode, schema);
26 changes: 26 additions & 0 deletions src/repository/shareCodeRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ShareCodeModel } from '../models/shareCode';
import moment from 'moment';
import { Types } from 'mongoose';

export class ShareCodeRepository {
public findByShareCode = async (shareCode: string) => {
return ShareCodeModel.findOne({
shareCode: shareCode,
isUsed: false,
expiredAt: { $gt: moment().toDate() },
});
};

public create = async (ticketId: Types.ObjectId, shareCode: string) =>
await ShareCodeModel.create({
ticketId: ticketId,
shareCode: shareCode,
expiredAt: moment()
.add(process.env.SHARE_CODE_EXPIRES, 'minute')
.toDate(),
isUsed: false,
});

public update = async (ticketId: Types.ObjectId) =>
await ShareCodeModel.updateOne({ ticketId: ticketId }, { isUsed: true });
}
4 changes: 2 additions & 2 deletions src/repository/ticketRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,10 @@ export class TicketRepository {
userId,
status: TicketStatus.unverified,
giverId: ticket.userId,
writeOffAt: moment().toDate(),
isPublished: false,
};

return await TicketModel.findOneAndUpdate(filter, update, updateOptions);
return TicketModel.findOneAndUpdate(filter, update, updateOptions);
};

private checkInvalidTicket = (
Expand Down
61 changes: 29 additions & 32 deletions src/service/ticketService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { VerifyTicketsDTO } from '../dto/ticket/verifyTicketsDto';
import { EditTicketsDTO } from '../dto/ticket/editTicketsDto';
import { CreateShareCodeDTO } from '../dto/ticket/createShareCodeDto';
import { Types } from 'mongoose';
import * as crypto from 'node:crypto';
import { TransferTicketDTO } from '../dto/ticket/transferTicketDto';
import { GetTicketDetailDto } from '../dto/ticket/getTicketDetailDto';
import { GetSharedTicketsDto } from '../dto/ticket/getSharedTicketsDto';
import { TicketRefundDto } from '../dto/ticket/TicketRefundDto';
import { OrderRepository } from '../repository/orderRepository';
import { ITicket } from '../models/ticket';
import { ShareCodeRepository } from '../repository/shareCodeRepository';
import { ProductRepository } from '../repository/productRepository';
import { GetOrderInfoVo } from '../vo/ticket/getOrderInfoVo';
import { IProduct } from '../models/product';
Expand All @@ -27,6 +27,8 @@ const logger = log4js.getLogger(`TicketService`);
export class TicketService {
private readonly ticketRepository: TicketRepository = new TicketRepository();
private readonly orderRepository: OrderRepository = new OrderRepository();
private readonly shareCodeRepository: ShareCodeRepository =
new ShareCodeRepository();
private readonly productRepository: ProductRepository =
new ProductRepository();

Expand Down Expand Up @@ -124,40 +126,33 @@ export class TicketService {
return tickets;
};

private encryptTicketId = (ticketId: Types.ObjectId) => {
// 確保密鑰是 32 字節長度 (AES-256)
const key = crypto
.createHash('sha256')
.update(process.env.SHARE_CODE_SECRET_KEY)
.digest();
// 初始化向量
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
// 加密
let encrypted = cipher.update(ticketId.toString(), 'utf8', 'base64');
encrypted += cipher.final('base64');
// 返回加密後的 ticket_id 及 iv,兩者用 ':' 分隔
return `${iv.toString('base64')}:${encrypted}`;
private checkShareCode = async (shareCode: string) => {
// 查出驗證碼並用查詢條件判斷是否過期
const code = await this.shareCodeRepository.findByShareCode(shareCode);
if (code) {
return code.ticketId;
} else {
throwError(
CustomResponseType.SHARE_CODE_ERROR_MESSAGE,
CustomResponseType.SHARE_CODE_ERROR,
);
}
};

private decryptShareCode = (shareCode: string) => {
const parts = shareCode.split(':');
const iv = Buffer.from(parts[0], 'base64');
const encrypted = parts[1];
const key = crypto
.createHash('sha256')
.update(process.env.SHARE_CODE_SECRET_KEY)
.digest();
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
private genShareCode = async (ticketId: Types.ObjectId) => {
let shareCode;
let code;
// 產生後檢查db是否有重複、未到期、未使用的驗證碼
do {
shareCode = (Math.floor(Math.random() * 9000000) + 1000000).toString();
code = await this.shareCodeRepository.findByShareCode(shareCode);
} while (code);
await this.shareCodeRepository.create(ticketId, shareCode.toString());
return shareCode;
};

public updateShareCode = async (createShareCodeDto: CreateShareCodeDTO) => {
const { ticketId } = createShareCodeDto;
const shareCode = this.encryptTicketId(ticketId);
createShareCodeDto.shareCode = shareCode;
createShareCodeDto.shareCode = await this.genShareCode(ticketId);
const ticket =
await this.ticketRepository.updateShareCode(createShareCodeDto);

Expand All @@ -173,17 +168,19 @@ export class TicketService {

public transferTicket = async (transferTicketDto: TransferTicketDTO) => {
const { shareCode } = transferTicketDto;
const ticketId = this.decryptShareCode(shareCode);
const ticketId = (await this.checkShareCode(shareCode)) as Types.ObjectId;
const ticket = await this.ticketRepository.transferTicket(
transferTicketDto,
new Types.ObjectId(ticketId),
ticketId,
);

if (!ticket) {
throwError(
CustomResponseType.TRANSFER_TICKET_ERROR_MESSAGE + '無效的分票驗證碼',
CustomResponseType.TRANSFER_TICKET_ERROR,
);
} else {
await this.shareCodeRepository.update(ticketId);
}
return ticket;
};
Expand Down
2 changes: 2 additions & 0 deletions src/swagger/definition/group/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const UserGroupItem = {
participant: [
{
userId: '123123aabb',
phone: '0912345678',
name: '阿明',
nickName: '小明',
lineId: '1234567',
Expand Down Expand Up @@ -69,6 +70,7 @@ export const GetGroupDetailSuccess = {
participant: [
{
userId: '123123aabb',
phone: '0912345678',
name: '阿明',
nickName: '小明',
lineId: '1234567',
Expand Down
6 changes: 3 additions & 3 deletions src/swagger/definition/ticket/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const GetTicketsSuccess = {
{
...Ticket,
product: TicketProductDetail,
shareCode: '112315641231',
shareCode: '1234567',
},
],
...PaginationSuccess,
Expand Down Expand Up @@ -130,7 +130,7 @@ export const EditTicketsSuccess = {
$status: CustomResponseType.OK,
$message: CustomResponseType.OK_MESSAGE,
$data: {
$tickets: [{ ...Ticket, shareCode: '112315641231' }],
$tickets: [{ ...Ticket, shareCode: '1234567' }],
},
};

Expand All @@ -139,7 +139,7 @@ export const TransferTicketSuccess = {
$message: CustomResponseType.OK_MESSAGE,
$data: {
...Ticket,
$shareCode: '112315641231',
$shareCode: '1234567',
},
};
export const GetTransferTicketSuccess = {
Expand Down
3 changes: 3 additions & 0 deletions src/types/customResponseType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,7 @@ export const enum CustomResponseType {

INVALID_TICKET_REFUND = '6548',
INVALID_TICKET_REFUND_MESSAGE = '退票錯誤',

SHARE_CODE_ERROR = '6549',
SHARE_CODE_ERROR_MESSAGE = '分票驗證碼錯誤',
}

0 comments on commit 6c2a534

Please sign in to comment.