diff --git a/docs/api/orders.yaml b/docs/api/orders.yaml new file mode 100644 index 0000000..861b561 --- /dev/null +++ b/docs/api/orders.yaml @@ -0,0 +1,396 @@ +openapi: 3.0.0 +info: + title: 订单管理API + description: 订单管理相关接口文档 + version: 1.0.0 + +paths: + /api/orders/: + get: + summary: 获取订单列表 + description: 获取系统中的订单列表,支持分页 + parameters: + - name: skip + in: query + description: 跳过的记录数 + required: false + schema: + type: integer + default: 0 + - name: limit + in: query + description: 返回的记录数 + required: false + schema: + type: integer + default: 100 + responses: + '200': + description: 成功返回订单列表 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Order' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: 创建订单 + description: 创建一个新的订单 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderCreate' + responses: + '200': + description: 成功创建订单 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/orders/{order_id}: + get: + summary: 获取订单详情 + description: 根据订单ID获取订单详细信息 + parameters: + - name: order_id + in: path + description: 订单ID + required: true + schema: + type: integer + responses: + '200': + description: 成功返回订单信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '404': + description: 订单未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: 更新订单信息 + description: 根据订单ID更新订单信息 + parameters: + - name: order_id + in: path + description: 订单ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderUpdate' + responses: + '200': + description: 成功更新订单信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '404': + description: 订单未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + summary: 删除订单 + description: 根据订单ID删除订单 + parameters: + - name: order_id + in: path + description: 订单ID + required: true + schema: + type: integer + responses: + '200': + description: 成功删除订单 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '404': + description: 订单未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + Order: + type: object + properties: + id: + type: integer + description: 订单ID + order_no: + type: string + description: 订单号 + buyer_id: + type: integer + description: 买家ID + seller_id: + type: integer + description: 卖家ID + variety_type: + type: string + description: 品种类型 + weight_range: + type: string + description: 重量范围 + weight_actual: + type: number + format: float + description: 实际重量 + price_per_unit: + type: number + format: float + description: 单价 + total_price: + type: number + format: float + description: 总价 + advance_payment: + type: number + format: float + description: 预付款 + final_payment: + type: number + format: float + description: 尾款 + status: + type: string + enum: [pending, confirmed, processing, shipped, delivered, cancelled, completed] + description: 订单状态 + delivery_address: + type: string + description: 收货地址 + delivery_time: + type: string + format: date-time + description: 交付时间 + remark: + type: string + description: 备注 + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - id + - order_no + - buyer_id + - seller_id + - variety_type + - weight_range + - price_per_unit + - total_price + - advance_payment + - final_payment + - status + - created_at + - updated_at + + OrderCreate: + type: object + properties: + buyer_id: + type: integer + description: 买家ID + seller_id: + type: integer + description: 卖家ID + variety_type: + type: string + description: 品种类型 + weight_range: + type: string + description: 重量范围 + weight_actual: + type: number + format: float + description: 实际重量 + price_per_unit: + type: number + format: float + description: 单价 + total_price: + type: number + format: float + description: 总价 + advance_payment: + type: number + format: float + description: 预付款 + final_payment: + type: number + format: float + description: 尾款 + status: + type: string + enum: [pending, confirmed, processing, shipped, delivered, cancelled, completed] + description: 订单状态 + delivery_address: + type: string + description: 收货地址 + delivery_time: + type: string + format: date-time + description: 交付时间 + remark: + type: string + description: 备注 + required: + - buyer_id + - seller_id + - variety_type + - weight_range + - price_per_unit + - total_price + + OrderUpdate: + type: object + properties: + buyer_id: + type: integer + description: 买家ID + seller_id: + type: integer + description: 卖家ID + variety_type: + type: string + description: 品种类型 + weight_range: + type: string + description: 重量范围 + weight_actual: + type: number + format: float + description: 实际重量 + price_per_unit: + type: number + format: float + description: 单价 + total_price: + type: number + format: float + description: 总价 + advance_payment: + type: number + format: float + description: 预付款 + final_payment: + type: number + format: float + description: 尾款 + status: + type: string + enum: [pending, confirmed, processing, shipped, delivered, cancelled, completed] + description: 订单状态 + delivery_address: + type: string + description: 收货地址 + delivery_time: + type: string + format: date-time + description: 交付时间 + remark: + type: string + description: 备注 + + Error: + type: object + properties: + detail: + type: string + description: 错误信息 + required: + - detail \ No newline at end of file diff --git a/docs/api/payments.yaml b/docs/api/payments.yaml new file mode 100644 index 0000000..7a78fb1 --- /dev/null +++ b/docs/api/payments.yaml @@ -0,0 +1,329 @@ +openapi: 3.0.0 +info: + title: 支付管理API + description: 支付管理相关接口文档 + version: 1.0.0 + +paths: + /api/payments/: + get: + summary: 获取支付列表 + description: 获取系统中的支付列表,支持分页 + parameters: + - name: skip + in: query + description: 跳过的记录数 + required: false + schema: + type: integer + default: 0 + - name: limit + in: query + description: 返回的记录数 + required: false + schema: + type: integer + default: 100 + responses: + '200': + description: 成功返回支付列表 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Payment' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: 创建支付 + description: 创建一个新的支付记录 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PaymentCreate' + responses: + '200': + description: 成功创建支付 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/payments/{payment_id}: + get: + summary: 获取支付详情 + description: 根据支付ID获取支付详细信息 + parameters: + - name: payment_id + in: path + description: 支付ID + required: true + schema: + type: integer + responses: + '200': + description: 成功返回支付信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '404': + description: 支付未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: 更新支付信息 + description: 根据支付ID更新支付信息 + parameters: + - name: payment_id + in: path + description: 支付ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PaymentUpdate' + responses: + '200': + description: 成功更新支付信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '404': + description: 支付未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + summary: 删除支付 + description: 根据支付ID删除支付 + parameters: + - name: payment_id + in: path + description: 支付ID + required: true + schema: + type: integer + responses: + '200': + description: 成功删除支付 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '404': + description: 支付未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + Payment: + type: object + properties: + id: + type: integer + description: 支付ID + payment_no: + type: string + description: 支付编号 + order_id: + type: integer + description: 订单ID + user_id: + type: integer + description: 用户ID + amount: + type: number + format: float + description: 支付金额 + payment_type: + type: string + enum: [advance, final] + description: 支付类型 + payment_method: + type: string + enum: [wechat, alipay, bank] + description: 支付方式 + status: + type: string + enum: [pending, success, failed, refunded] + description: 支付状态 + transaction_id: + type: string + description: 交易ID + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - id + - payment_no + - order_id + - user_id + - amount + - payment_type + - payment_method + - status + - created_at + - updated_at + + PaymentCreate: + type: object + properties: + order_id: + type: integer + description: 订单ID + user_id: + type: integer + description: 用户ID + amount: + type: number + format: float + description: 支付金额 + payment_type: + type: string + enum: [advance, final] + description: 支付类型 + payment_method: + type: string + enum: [wechat, alipay, bank] + description: 支付方式 + status: + type: string + enum: [pending, success, failed, refunded] + description: 支付状态 + transaction_id: + type: string + description: 交易ID + required: + - order_id + - user_id + - amount + - payment_type + - payment_method + + PaymentUpdate: + type: object + properties: + order_id: + type: integer + description: 订单ID + user_id: + type: integer + description: 用户ID + amount: + type: number + format: float + description: 支付金额 + payment_type: + type: string + enum: [advance, final] + description: 支付类型 + payment_method: + type: string + enum: [wechat, alipay, bank] + description: 支付方式 + status: + type: string + enum: [pending, success, failed, refunded] + description: 支付状态 + transaction_id: + type: string + description: 交易ID + + Error: + type: object + properties: + detail: + type: string + description: 错误信息 + required: + - detail \ No newline at end of file diff --git a/docs/api/users.yaml b/docs/api/users.yaml new file mode 100644 index 0000000..14c5cf3 --- /dev/null +++ b/docs/api/users.yaml @@ -0,0 +1,229 @@ +openapi: 3.0.0 +info: + title: 用户管理API + description: 用户管理相关接口文档 + version: 1.0.0 + +paths: + /api/users/: + get: + summary: 获取用户列表 + description: 获取系统中的用户列表,支持分页 + parameters: + - name: skip + in: query + description: 跳过的记录数 + required: false + schema: + type: integer + default: 0 + - name: limit + in: query + description: 返回的记录数 + required: false + schema: + type: integer + default: 100 + responses: + '200': + description: 成功返回用户列表 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/{user_id}: + get: + summary: 获取用户详情 + description: 根据用户ID获取用户详细信息 + parameters: + - name: user_id + in: path + description: 用户ID + required: true + schema: + type: integer + responses: + '200': + description: 成功返回用户信息 + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: 用户未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: 更新用户信息 + description: 根据用户ID更新用户信息 + parameters: + - name: user_id + in: path + description: 用户ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdate' + responses: + '200': + description: 成功更新用户信息 + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: 用户未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + summary: 删除用户 + description: 根据用户ID删除用户 + parameters: + - name: user_id + in: path + description: 用户ID + required: true + schema: + type: integer + responses: + '200': + description: 成功删除用户 + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: 用户未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + User: + type: object + properties: + id: + type: integer + description: 用户ID + uuid: + type: string + description: 用户UUID + username: + type: string + description: 用户名 + user_type: + type: string + enum: [client, supplier, driver, staff, admin] + description: 用户类型 + status: + type: string + enum: [active, inactive, locked] + description: 用户状态 + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - id + - uuid + - username + - user_type + - status + - created_at + - updated_at + + UserUpdate: + type: object + properties: + username: + type: string + description: 用户名 + user_type: + type: string + enum: [client, supplier, driver, staff, admin] + description: 用户类型 + status: + type: string + enum: [active, inactive, locked] + description: 用户状态 + + Error: + type: object + properties: + detail: + type: string + description: 错误信息 + required: + - detail \ No newline at end of file diff --git a/fastapi-backend/.env b/fastapi-backend/.env new file mode 100644 index 0000000..f4b9f40 --- /dev/null +++ b/fastapi-backend/.env @@ -0,0 +1,15 @@ +# Database configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_mysql_password +DB_NAME=niumall_dev + +# JWT configuration +JWT_SECRET=niumall-jwt-secret +JWT_ALGORITHM=HS256 +JWT_EXPIRATION_MINUTES=1440 + +# Server configuration +SERVER_HOST=0.0.0.0 +SERVER_PORT=8000 \ No newline at end of file diff --git a/fastapi-backend/.env.example b/fastapi-backend/.env.example new file mode 100644 index 0000000..efa3259 --- /dev/null +++ b/fastapi-backend/.env.example @@ -0,0 +1,15 @@ +# Database configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=password +DB_NAME=niumall_dev + +# JWT configuration +JWT_SECRET=niumall-jwt-secret +JWT_ALGORITHM=HS256 +JWT_EXPIRATION_MINUTES=1440 + +# Server configuration +SERVER_HOST=0.0.0.0 +SERVER_PORT=8000 \ No newline at end of file diff --git a/fastapi-backend/README.md b/fastapi-backend/README.md new file mode 100644 index 0000000..ece2e21 --- /dev/null +++ b/fastapi-backend/README.md @@ -0,0 +1,42 @@ +# FastAPI Backend for 活牛采购智能数字化系统 + +This is a FastAPI implementation of the backend for the 活牛采购智能数字化系统. + +## Features +- User authentication with JWT +- Order management +- Payment processing +- Role-based access control +- Database integration with SQLAlchemy +- API documentation with Swagger UI and ReDoc + +## Requirements +- Python 3.8+ +- FastAPI +- SQLAlchemy +- PyJWT +- Uvicorn + +## Installation +1. Create a virtual environment: + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Configure environment variables (see .env.example) + +4. Run the application: + ```bash + uvicorn app.main:app --reload + ``` + +## API Documentation +Once the server is running, you can access: +- Swagger UI: http://localhost:8000/docs +- ReDoc: http://localhost:8000/redoc \ No newline at end of file diff --git a/fastapi-backend/app/__init__.py b/fastapi-backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-backend/app/api/__init__.py b/fastapi-backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-backend/app/api/auth.py b/fastapi-backend/app/api/auth.py new file mode 100644 index 0000000..af097e4 --- /dev/null +++ b/fastapi-backend/app/api/auth.py @@ -0,0 +1,80 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer +from sqlalchemy.orm import Session +from app.db.database import get_db +from app.models.user import User +from app.schemas.user import UserCreate, Token, User +from app.core.security import verify_password, create_access_token, decode_access_token +from datetime import timedelta +from app.core.config import settings +import uuid + +router = APIRouter() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") + +@router.post("/login", response_model=Token) +def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + # Find user by username + user = db.query(User).filter(User.username == form_data.username).first() + if not user or not verify_password(form_data.password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Create access token + access_token_expires = timedelta(minutes=settings.JWT_EXPIRATION_MINUTES) + access_token = create_access_token( + data={"sub": user.uuid}, expires_delta=access_token_expires + ) + + return {"access_token": access_token, "token_type": "bearer"} + +@router.post("/register", response_model=User) +def register(user: UserCreate, db: Session = Depends(get_db)): + # Check if user already exists + db_user = db.query(User).filter(User.username == user.username).first() + if db_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered", + ) + + # Create new user + new_user = User( + uuid=str(uuid.uuid4()), + username=user.username, + user_type=user.user_type, + status=user.status + ) + new_user.set_password(user.password) + + db.add(new_user) + db.commit() + db.refresh(new_user) + + return new_user + +@router.get("/me", response_model=User) +def read_users_me(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + payload = decode_access_token(token) + if payload is None: + raise credentials_exception + + uuid: str = payload.get("sub") + if uuid is None: + raise credentials_exception + + user = db.query(User).filter(User.uuid == uuid).first() + if user is None: + raise credentials_exception + + return user \ No newline at end of file diff --git a/fastapi-backend/app/api/orders.py b/fastapi-backend/app/api/orders.py new file mode 100644 index 0000000..f568a39 --- /dev/null +++ b/fastapi-backend/app/api/orders.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from app.db.database import get_db +from app.models.order import Order +from app.schemas.order import Order, OrderCreate, OrderUpdate +import uuid + +router = APIRouter() + +@router.get("/", response_model=List[Order]) +def read_orders(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + orders = db.query(Order).offset(skip).limit(limit).all() + return orders + +@router.get("/{order_id}", response_model=Order) +def read_order(order_id: int, db: Session = Depends(get_db)): + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + return db_order + +@router.post("/", response_model=Order) +def create_order(order: OrderCreate, db: Session = Depends(get_db)): + # Generate unique order number + order_no = f"ORD{uuid.uuid4().hex[:16].upper()}" + + db_order = Order( + order_no=order_no, + buyer_id=order.buyer_id, + seller_id=order.seller_id, + variety_type=order.variety_type, + weight_range=order.weight_range, + weight_actual=order.weight_actual, + price_per_unit=order.price_per_unit, + total_price=order.total_price, + advance_payment=order.advance_payment, + final_payment=order.final_payment, + status=order.status, + delivery_address=order.delivery_address, + delivery_time=order.delivery_time, + remark=order.remark + ) + + db.add(db_order) + db.commit() + db.refresh(db_order) + return db_order + +@router.put("/{order_id}", response_model=Order) +def update_order(order_id: int, order: OrderUpdate, db: Session = Depends(get_db)): + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + + # Update order fields + for field, value in order.dict(exclude_unset=True).items(): + setattr(db_order, field, value) + + db.commit() + db.refresh(db_order) + return db_order + +@router.delete("/{order_id}", response_model=Order) +def delete_order(order_id: int, db: Session = Depends(get_db)): + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + + db.delete(db_order) + db.commit() + return db_order \ No newline at end of file diff --git a/fastapi-backend/app/api/payments.py b/fastapi-backend/app/api/payments.py new file mode 100644 index 0000000..7f6f07a --- /dev/null +++ b/fastapi-backend/app/api/payments.py @@ -0,0 +1,66 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from app.db.database import get_db +from app.models.payment import Payment +from app.schemas.payment import Payment, PaymentCreate, PaymentUpdate +import uuid + +router = APIRouter() + +@router.get("/", response_model=List[Payment]) +def read_payments(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + payments = db.query(Payment).offset(skip).limit(limit).all() + return payments + +@router.get("/{payment_id}", response_model=Payment) +def read_payment(payment_id: int, db: Session = Depends(get_db)): + db_payment = db.query(Payment).filter(Payment.id == payment_id).first() + if db_payment is None: + raise HTTPException(status_code=404, detail="Payment not found") + return db_payment + +@router.post("/", response_model=Payment) +def create_payment(payment: PaymentCreate, db: Session = Depends(get_db)): + # Generate unique payment number + payment_no = f"PMT{uuid.uuid4().hex[:16].upper()}" + + db_payment = Payment( + payment_no=payment_no, + order_id=payment.order_id, + user_id=payment.user_id, + amount=payment.amount, + payment_type=payment.payment_type, + payment_method=payment.payment_method, + status=payment.status, + transaction_id=payment.transaction_id + ) + + db.add(db_payment) + db.commit() + db.refresh(db_payment) + return db_payment + +@router.put("/{payment_id}", response_model=Payment) +def update_payment(payment_id: int, payment: PaymentUpdate, db: Session = Depends(get_db)): + db_payment = db.query(Payment).filter(Payment.id == payment_id).first() + if db_payment is None: + raise HTTPException(status_code=404, detail="Payment not found") + + # Update payment fields + for field, value in payment.dict(exclude_unset=True).items(): + setattr(db_payment, field, value) + + db.commit() + db.refresh(db_payment) + return db_payment + +@router.delete("/{payment_id}", response_model=Payment) +def delete_payment(payment_id: int, db: Session = Depends(get_db)): + db_payment = db.query(Payment).filter(Payment.id == payment_id).first() + if db_payment is None: + raise HTTPException(status_code=404, detail="Payment not found") + + db.delete(db_payment) + db.commit() + return db_payment \ No newline at end of file diff --git a/fastapi-backend/app/api/users.py b/fastapi-backend/app/api/users.py new file mode 100644 index 0000000..d9093ab --- /dev/null +++ b/fastapi-backend/app/api/users.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from app.db.database import get_db +from app.models.user import User +from app.schemas.user import User, UserCreate, UserUpdate +from app.api.auth import oauth2_scheme + +router = APIRouter() + +@router.get("/", response_model=List[User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = db.query(User).offset(skip).limit(limit).all() + return users + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + +@router.put("/{user_id}", response_model=User) +def update_user(user_id: int, user: UserUpdate, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + + # Update user fields + if user.username is not None: + db_user.username = user.username + if user.user_type is not None: + db_user.user_type = user.user_type + if user.status is not None: + db_user.status = user.status + + db.commit() + db.refresh(db_user) + return db_user + +@router.delete("/{user_id}", response_model=User) +def delete_user(user_id: int, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + + db.delete(db_user) + db.commit() + return db_user \ No newline at end of file diff --git a/fastapi-backend/app/core/__init__.py b/fastapi-backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-backend/app/core/config.py b/fastapi-backend/app/core/config.py new file mode 100644 index 0000000..4f1d288 --- /dev/null +++ b/fastapi-backend/app/core/config.py @@ -0,0 +1,24 @@ +import os +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + # Database configuration + DB_HOST: str = os.getenv("DB_HOST", "localhost") + DB_PORT: int = int(os.getenv("DB_PORT", 3306)) + DB_USER: str = os.getenv("DB_USER", "root") + DB_PASSWORD: str = os.getenv("DB_PASSWORD", "password") + DB_NAME: str = os.getenv("DB_NAME", "niumall_dev") + + # JWT configuration + JWT_SECRET: str = os.getenv("JWT_SECRET", "niumall-jwt-secret") + JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256") + JWT_EXPIRATION_MINUTES: int = int(os.getenv("JWT_EXPIRATION_MINUTES", 1440)) + + # Server configuration + SERVER_HOST: str = os.getenv("SERVER_HOST", "0.0.0.0") + SERVER_PORT: int = int(os.getenv("SERVER_PORT", 8000)) + + class Config: + env_file = ".env" + +settings = Settings() \ No newline at end of file diff --git a/fastapi-backend/app/core/security.py b/fastapi-backend/app/core/security.py new file mode 100644 index 0000000..40d49ca --- /dev/null +++ b/fastapi-backend/app/core/security.py @@ -0,0 +1,30 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRATION_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) + return encoded_jwt + +def decode_access_token(token: str): + try: + payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM]) + return payload + except JWTError: + return None \ No newline at end of file diff --git a/fastapi-backend/app/db/__init__.py b/fastapi-backend/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-backend/app/db/database.py b/fastapi-backend/app/db/database.py new file mode 100644 index 0000000..3374edf --- /dev/null +++ b/fastapi-backend/app/db/database.py @@ -0,0 +1,24 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from app.core.config import settings + +# Create database URL +SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}" + +# Create engine +engine = create_engine(SQLALCHEMY_DATABASE_URL, echo=True, pool_pre_ping=True) + +# Create session +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base class for models +Base = declarative_base() + +# Dependency to get DB session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/fastapi-backend/app/main.py b/fastapi-backend/app/main.py new file mode 100644 index 0000000..f1f1449 --- /dev/null +++ b/fastapi-backend/app/main.py @@ -0,0 +1,45 @@ +from fastapi import FastAPI, Request, Depends +from fastapi.middleware.cors import CORSMiddleware +from app.api import auth, users, orders, payments +from app.core.config import settings +from app.db.database import engine, Base +from app.middleware.error_handler import error_handler +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException + +# Create database tables +Base.metadata.create_all(bind=engine) + +app = FastAPI( + title="活牛采购智能数字化系统API", + description="活牛采购智能数字化系统后端API接口", + version="1.0.0", +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, specify exact origins + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Add exception handlers +app.add_exception_handler(StarletteHTTPException, error_handler) +app.add_exception_handler(RequestValidationError, error_handler) +app.add_exception_handler(Exception, error_handler) + +# Include routers +app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) +app.include_router(users.router, prefix="/api/users", tags=["users"]) +app.include_router(orders.router, prefix="/api/orders", tags=["orders"]) +app.include_router(payments.router, prefix="/api/payments", tags=["payments"]) + +@app.get("/") +async def root(): + return {"message": "活牛采购智能数字化系统后端服务", "version": "1.0.0"} + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} \ No newline at end of file diff --git a/fastapi-backend/app/middleware/__init__.py b/fastapi-backend/app/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-backend/app/middleware/auth.py b/fastapi-backend/app/middleware/auth.py new file mode 100644 index 0000000..dd45048 --- /dev/null +++ b/fastapi-backend/app/middleware/auth.py @@ -0,0 +1,39 @@ +from fastapi import HTTPException, status, Depends +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session +from app.db.database import get_db +from app.models.user import User +from app.core.security import decode_access_token + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + payload = decode_access_token(token) + if payload is None: + raise credentials_exception + + uuid: str = payload.get("sub") + if uuid is None: + raise credentials_exception + + user = db.query(User).filter(User.uuid == uuid).first() + if user is None: + raise credentials_exception + + return user + +def require_role(required_role: str): + def role_checker(current_user: User = Depends(get_current_user)): + if current_user.user_type != required_role: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient permissions", + ) + return current_user + return role_checker \ No newline at end of file diff --git a/fastapi-backend/app/middleware/error_handler.py b/fastapi-backend/app/middleware/error_handler.py new file mode 100644 index 0000000..56b225a --- /dev/null +++ b/fastapi-backend/app/middleware/error_handler.py @@ -0,0 +1,25 @@ +from fastapi import HTTPException, Request +from fastapi.responses import JSONResponse +from app.utils.response import ErrorResponse + +async def error_handler(request: Request, exc: Exception): + # Handle HTTP exceptions + if isinstance(exc, HTTPException): + return JSONResponse( + status_code=exc.status_code, + content=ErrorResponse( + code=exc.status_code, + message=exc.detail, + data=None + ).dict() + ) + + # Handle general exceptions + return JSONResponse( + status_code=500, + content=ErrorResponse( + code=500, + message="Internal Server Error", + data=str(exc) + ).dict() + ) \ No newline at end of file diff --git a/fastapi-backend/app/models/__init__.py b/fastapi-backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-backend/app/models/order.py b/fastapi-backend/app/models/order.py new file mode 100644 index 0000000..0b9d26b --- /dev/null +++ b/fastapi-backend/app/models/order.py @@ -0,0 +1,33 @@ +from sqlalchemy import Column, Integer, String, Float, Enum, DateTime, Text, func, ForeignKey +from app.db.database import Base +from enum import Enum as PyEnum + +class OrderStatus(str, PyEnum): + PENDING = "pending" + CONFIRMED = "confirmed" + PROCESSING = "processing" + SHIPPED = "shipped" + DELIVERED = "delivered" + CANCELLED = "cancelled" + COMPLETED = "completed" + +class Order(Base): + __tablename__ = "orders" + + id = Column(Integer, primary_key=True, index=True) + order_no = Column(String(32), unique=True, index=True, nullable=False) + buyer_id = Column(Integer, ForeignKey("users.id"), nullable=False) + seller_id = Column(Integer, ForeignKey("users.id"), nullable=False) + variety_type = Column(String(50), nullable=False) + weight_range = Column(String(50), nullable=False) + weight_actual = Column(Float, nullable=True) + price_per_unit = Column(Float, nullable=False) + total_price = Column(Float, nullable=False) + advance_payment = Column(Float, nullable=False, default=0.0) + final_payment = Column(Float, nullable=False, default=0.0) + status = Column(Enum(OrderStatus), default=OrderStatus.PENDING, nullable=False) + delivery_address = Column(Text, nullable=True) + delivery_time = Column(DateTime, nullable=True) + remark = Column(Text, nullable=True) + created_at = Column(DateTime, default=func.now(), nullable=False) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) \ No newline at end of file diff --git a/fastapi-backend/app/models/payment.py b/fastapi-backend/app/models/payment.py new file mode 100644 index 0000000..4e541f6 --- /dev/null +++ b/fastapi-backend/app/models/payment.py @@ -0,0 +1,33 @@ +from sqlalchemy import Column, Integer, String, Float, Enum, DateTime, ForeignKey, func +from app.db.database import Base +from enum import Enum as PyEnum + +class PaymentType(str, PyEnum): + ADVANCE = "advance" + FINAL = "final" + +class PaymentMethod(str, PyEnum): + WECHAT = "wechat" + ALIPAY = "alipay" + BANK = "bank" + +class PaymentStatus(str, PyEnum): + PENDING = "pending" + SUCCESS = "success" + FAILED = "failed" + REFUNDED = "refunded" + +class Payment(Base): + __tablename__ = "payments" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("orders.id"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + payment_no = Column(String(32), unique=True, index=True, nullable=False) + amount = Column(Float, nullable=False) + payment_type = Column(Enum(PaymentType), nullable=False) + payment_method = Column(Enum(PaymentMethod), nullable=False) + status = Column(Enum(PaymentStatus), default=PaymentStatus.PENDING, nullable=False) + transaction_id = Column(String(128), nullable=True) + created_at = Column(DateTime, default=func.now(), nullable=False) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) \ No newline at end of file diff --git a/fastapi-backend/app/models/user.py b/fastapi-backend/app/models/user.py new file mode 100644 index 0000000..ed84dab --- /dev/null +++ b/fastapi-backend/app/models/user.py @@ -0,0 +1,36 @@ +from sqlalchemy import Column, Integer, String, Enum, DateTime, func +from app.db.database import Base +from passlib.context import CryptContext +from enum import Enum as PyEnum + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +class UserType(str, PyEnum): + CLIENT = "client" + SUPPLIER = "supplier" + DRIVER = "driver" + STAFF = "staff" + ADMIN = "admin" + +class UserStatus(str, PyEnum): + ACTIVE = "active" + INACTIVE = "inactive" + LOCKED = "locked" + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + uuid = Column(String(36), unique=True, index=True, nullable=False) + username = Column(String(50), unique=True, index=True, nullable=False) + password_hash = Column(String(128), nullable=False) + user_type = Column(Enum(UserType), default=UserType.CLIENT, nullable=False) + status = Column(Enum(UserStatus), default=UserStatus.ACTIVE, nullable=False) + created_at = Column(DateTime, default=func.now(), nullable=False) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) + + def verify_password(self, plain_password): + return pwd_context.verify(plain_password, self.password_hash) + + def set_password(self, plain_password): + self.password_hash = pwd_context.hash(plain_password) \ No newline at end of file diff --git a/fastapi-backend/app/schemas/__init__.py b/fastapi-backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-backend/app/schemas/order.py b/fastapi-backend/app/schemas/order.py new file mode 100644 index 0000000..b4eecb7 --- /dev/null +++ b/fastapi-backend/app/schemas/order.py @@ -0,0 +1,58 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime +from enum import Enum + +class OrderStatus(str, Enum): + PENDING = "pending" + CONFIRMED = "confirmed" + PROCESSING = "processing" + SHIPPED = "shipped" + DELIVERED = "delivered" + CANCELLED = "cancelled" + COMPLETED = "completed" + +class OrderBase(BaseModel): + buyer_id: int + seller_id: int + variety_type: str + weight_range: str + weight_actual: Optional[float] = None + price_per_unit: float + total_price: float + advance_payment: float = 0.0 + final_payment: float = 0.0 + status: OrderStatus = OrderStatus.PENDING + delivery_address: Optional[str] = None + delivery_time: Optional[datetime] = None + remark: Optional[str] = None + +class OrderCreate(OrderBase): + pass + +class OrderUpdate(BaseModel): + buyer_id: Optional[int] = None + seller_id: Optional[int] = None + variety_type: Optional[str] = None + weight_range: Optional[str] = None + weight_actual: Optional[float] = None + price_per_unit: Optional[float] = None + total_price: Optional[float] = None + advance_payment: Optional[float] = None + final_payment: Optional[float] = None + status: Optional[OrderStatus] = None + delivery_address: Optional[str] = None + delivery_time: Optional[datetime] = None + remark: Optional[str] = None + +class OrderInDBBase(OrderBase): + id: int + order_no: str + created_at: datetime + updated_at: datetime + + class Config: + orm_mode = True + +class Order(OrderInDBBase): + pass \ No newline at end of file diff --git a/fastapi-backend/app/schemas/payment.py b/fastapi-backend/app/schemas/payment.py new file mode 100644 index 0000000..b19da9f --- /dev/null +++ b/fastapi-backend/app/schemas/payment.py @@ -0,0 +1,52 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime +from enum import Enum + +class PaymentType(str, Enum): + ADVANCE = "advance" + FINAL = "final" + +class PaymentMethod(str, Enum): + WECHAT = "wechat" + ALIPAY = "alipay" + BANK = "bank" + +class PaymentStatus(str, Enum): + PENDING = "pending" + SUCCESS = "success" + FAILED = "failed" + REFUNDED = "refunded" + +class PaymentBase(BaseModel): + order_id: int + user_id: int + amount: float + payment_type: PaymentType + payment_method: PaymentMethod + status: PaymentStatus = PaymentStatus.PENDING + transaction_id: Optional[str] = None + +class PaymentCreate(PaymentBase): + pass + +class PaymentUpdate(BaseModel): + order_id: Optional[int] = None + user_id: Optional[int] = None + amount: Optional[float] = None + payment_type: Optional[PaymentType] = None + payment_method: Optional[PaymentMethod] = None + status: Optional[PaymentStatus] = None + transaction_id: Optional[str] = None + +class PaymentInDBBase(PaymentBase): + id: int + payment_no: str + created_at: datetime + updated_at: datetime + + class Config: + orm_mode = True + +class Payment(PaymentInDBBase): + pass \ No newline at end of file diff --git a/fastapi-backend/app/schemas/user.py b/fastapi-backend/app/schemas/user.py new file mode 100644 index 0000000..7b31e57 --- /dev/null +++ b/fastapi-backend/app/schemas/user.py @@ -0,0 +1,51 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime +from enum import Enum + +class UserType(str, Enum): + CLIENT = "client" + SUPPLIER = "supplier" + DRIVER = "driver" + STAFF = "staff" + ADMIN = "admin" + +class UserStatus(str, Enum): + ACTIVE = "active" + INACTIVE = "inactive" + LOCKED = "locked" + +class UserBase(BaseModel): + username: str + user_type: UserType = UserType.CLIENT + status: UserStatus = UserStatus.ACTIVE + +class UserCreate(UserBase): + password: str + +class UserUpdate(BaseModel): + username: Optional[str] = None + user_type: Optional[UserType] = None + status: Optional[UserStatus] = None + +class UserInDBBase(UserBase): + id: int + uuid: str + created_at: datetime + updated_at: datetime + + class Config: + orm_mode = True + +class User(UserInDBBase): + pass + +class UserInDB(UserInDBBase): + password_hash: str + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + uuid: Optional[str] = None \ No newline at end of file diff --git a/fastapi-backend/app/utils/response.py b/fastapi-backend/app/utils/response.py new file mode 100644 index 0000000..5212ff8 --- /dev/null +++ b/fastapi-backend/app/utils/response.py @@ -0,0 +1,26 @@ +from typing import Any, Optional, Generic, TypeVar +from pydantic import BaseModel +from datetime import datetime + +T = TypeVar('T') + +class SuccessResponse(BaseModel, Generic[T]): + code: int = 200 + message: str = "Success" + data: Optional[T] = None + timestamp: datetime = datetime.now() + +class ErrorResponse(BaseModel): + code: int = 500 + message: str = "Internal Server Error" + data: Optional[Any] = None + timestamp: datetime = datetime.now() + +class PaginatedResponse(BaseModel, Generic[T]): + code: int = 200 + message: str = "Success" + data: Optional[T] = None + timestamp: datetime = datetime.now() + total: int + page: int + limit: int \ No newline at end of file diff --git a/fastapi-backend/requirements.txt b/fastapi-backend/requirements.txt new file mode 100644 index 0000000..6dcf563 --- /dev/null +++ b/fastapi-backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.68.0 +uvicorn[standard]==0.15.0 +sqlalchemy==1.4.23 +databases[mysql]==0.5.3 +pydantic==1.8.2 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.5 +alembic==1.7.1 +python-dotenv==0.18.0 \ No newline at end of file diff --git a/go-backend/.env.example b/go-backend/.env.example new file mode 100644 index 0000000..7a39461 --- /dev/null +++ b/go-backend/.env.example @@ -0,0 +1,13 @@ +# Database configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD= +DB_NAME=niumall_go + +# Server configuration +PORT=8080 +GIN_MODE=debug + +# JWT configuration +JWT_SECRET=your-secret-key-here \ No newline at end of file diff --git a/go-backend/.gitignore b/go-backend/.gitignore new file mode 100644 index 0000000..e874029 --- /dev/null +++ b/go-backend/.gitignore @@ -0,0 +1,35 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with go test -c +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# Environment files +.env + +# IDE-specific files +.vscode/ +.idea/ + +# Docker +docker-compose.override.yml + +# Log files +*.log + +# Build outputs +main +bin/ \ No newline at end of file diff --git a/go-backend/Dockerfile b/go-backend/Dockerfile new file mode 100644 index 0000000..d5781da --- /dev/null +++ b/go-backend/Dockerfile @@ -0,0 +1,44 @@ +# 使用国内镜像源的Go镜像作为构建环境 +FROM golang:1.21-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 复制go.mod和go.sum文件 +COPY go.mod go.sum ./ + +# 为apk包管理器配置国内镜像源 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用 +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# 使用Alpine Linux作为运行环境 +FROM alpine:latest + +# 为apk包管理器配置国内镜像源 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories + +# 安装ca-certificates以支持HTTPS请求 +RUN apk --no-cache add ca-certificates + +# 设置工作目录 +WORKDIR /root/ + +# 从构建阶段复制二进制文件 +COPY --from=builder /app/main . + +# 复制环境配置文件 +COPY --from=builder /app/.env.example .env + +# 暴露端口 +EXPOSE 8080 + +# 运行应用 +CMD ["./main"] \ No newline at end of file diff --git a/go-backend/Makefile b/go-backend/Makefile new file mode 100644 index 0000000..fad93be --- /dev/null +++ b/go-backend/Makefile @@ -0,0 +1,50 @@ +# 定义变量 +BINARY_NAME=main +DOCKER_IMAGE_NAME=niumall-go-backend + +# 默认目标 +.PHONY: help +help: ## 显示帮助信息 + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: build +build: ## 构建Go应用 + go build -o ${BINARY_NAME} . + +.PHONY: run +run: ## 运行Go应用 + go run main.go + +.PHONY: test +test: ## 运行单元测试 + go test -v ./... + +.PHONY: clean +clean: ## 清理构建文件 + rm -f ${BINARY_NAME} + +.PHONY: docker-build +docker-build: ## 构建Docker镜像 + docker build -t ${DOCKER_IMAGE_NAME} . + +.PHONY: docker-run +docker-run: ## 运行Docker容器 + docker run -p 8080:8080 ${DOCKER_IMAGE_NAME} + +.PHONY: docker-compose-up +docker-compose-up: ## 使用docker-compose启动服务 + docker-compose up -d + +.PHONY: docker-compose-down +docker-compose-down: ## 使用docker-compose停止服务 + docker-compose down + +.PHONY: migrate-up +migrate-up: ## 运行数据库迁移(如果有的话) + @echo "运行数据库迁移..." + # 在这里添加数据库迁移命令 + +.PHONY: migrate-down +migrate-down: ## 回滚数据库迁移(如果有的话) + @echo "回滚数据库迁移..." + # 在这里添加数据库回滚命令 \ No newline at end of file diff --git a/go-backend/README.md b/go-backend/README.md new file mode 100644 index 0000000..7e4a6f6 --- /dev/null +++ b/go-backend/README.md @@ -0,0 +1,30 @@ +# Go Backend for Niumall + +This is the Go implementation of the Niumall backend using the Gin framework. + +## Project Structure + +``` +go-backend/ +├── README.md +├── go.mod +├── go.sum +├── main.go +├── config/ +├── models/ +├── routes/ +├── controllers/ +├── middleware/ +├── utils/ +└── docs/ +``` + +## Getting Started + +1. Install Go (version 1.16 or later) +2. Run `go mod tidy` to install dependencies +3. Run `go run main.go` to start the server + +## API Documentation + +API documentation is available in the `docs/` directory. \ No newline at end of file diff --git a/go-backend/config/database.go b/go-backend/config/database.go new file mode 100644 index 0000000..2b00d2a --- /dev/null +++ b/go-backend/config/database.go @@ -0,0 +1,33 @@ +package config + +import ( + "log" + "os" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func InitDB() { + var err error + + // Get database configuration from environment variables + host := os.Getenv("DB_HOST") + port := os.Getenv("DB_PORT") + user := os.Getenv("DB_USER") + password := os.Getenv("DB_PASSWORD") + dbname := os.Getenv("DB_NAME") + + // Create DSN (Data Source Name) + dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbname + "?charset=utf8mb4&parseTime=True&loc=Local" + + // Connect to database + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + + log.Println("Database connection established") +} \ No newline at end of file diff --git a/go-backend/controllers/order_controller.go b/go-backend/controllers/order_controller.go new file mode 100644 index 0000000..c319746 --- /dev/null +++ b/go-backend/controllers/order_controller.go @@ -0,0 +1,177 @@ +package controllers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" + + "niumall-go/config" + "niumall-go/models" +) + +// GetOrders retrieves a list of orders with pagination +func GetOrders(c *gin.Context) { + var orders []models.Order + skip, _ := strconv.Atoi(c.DefaultQuery("skip", "0")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100")) + + if err := config.DB.Offset(skip).Limit(limit).Find(&orders).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, orders) +} + +// GetOrderByID retrieves an order by ID +func GetOrderByID(c *gin.Context) { + id := c.Param("id") + var order models.Order + + if err := config.DB.First(&order, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, order) +} + +// CreateOrder creates a new order +func CreateOrder(c *gin.Context) { + var input models.Order + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Generate unique order number + orderNo := "ORD" + uuid.New().String()[:16] + + // Create order + order := models.Order{ + OrderNo: orderNo, + BuyerID: input.BuyerID, + SellerID: input.SellerID, + VarietyType: input.VarietyType, + WeightRange: input.WeightRange, + WeightActual: input.WeightActual, + PricePerUnit: input.PricePerUnit, + TotalPrice: input.TotalPrice, + AdvancePayment: input.AdvancePayment, + FinalPayment: input.FinalPayment, + Status: input.Status, + DeliveryAddress: input.DeliveryAddress, + DeliveryTime: input.DeliveryTime, + Remark: input.Remark, + } + + if err := config.DB.Create(&order).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, order) +} + +// UpdateOrder updates an order's information +func UpdateOrder(c *gin.Context) { + id := c.Param("id") + var order models.Order + + // Check if order exists + if err := config.DB.First(&order, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Parse input + var input models.Order + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update order fields + if input.BuyerID != 0 { + order.BuyerID = input.BuyerID + } + if input.SellerID != 0 { + order.SellerID = input.SellerID + } + if input.VarietyType != "" { + order.VarietyType = input.VarietyType + } + if input.WeightRange != "" { + order.WeightRange = input.WeightRange + } + if input.WeightActual != 0 { + order.WeightActual = input.WeightActual + } + if input.PricePerUnit != 0 { + order.PricePerUnit = input.PricePerUnit + } + if input.TotalPrice != 0 { + order.TotalPrice = input.TotalPrice + } + if input.AdvancePayment != 0 { + order.AdvancePayment = input.AdvancePayment + } + if input.FinalPayment != 0 { + order.FinalPayment = input.FinalPayment + } + if input.Status != "" { + order.Status = input.Status + } + if input.DeliveryAddress != "" { + order.DeliveryAddress = input.DeliveryAddress + } + if input.DeliveryTime != nil { + order.DeliveryTime = input.DeliveryTime + } + if input.Remark != "" { + order.Remark = input.Remark + } + + // Save updated order + if err := config.DB.Save(&order).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, order) +} + +// DeleteOrder deletes an order by ID +func DeleteOrder(c *gin.Context) { + id := c.Param("id") + var order models.Order + + // Check if order exists + if err := config.DB.First(&order, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Delete order + if err := config.DB.Delete(&order).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, order) +} \ No newline at end of file diff --git a/go-backend/controllers/payment_controller.go b/go-backend/controllers/payment_controller.go new file mode 100644 index 0000000..130c475 --- /dev/null +++ b/go-backend/controllers/payment_controller.go @@ -0,0 +1,153 @@ +package controllers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" + + "niumall-go/config" + "niumall-go/models" +) + +// GetPayments retrieves a list of payments with pagination +func GetPayments(c *gin.Context) { + var payments []models.Payment + skip, _ := strconv.Atoi(c.DefaultQuery("skip", "0")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100")) + + if err := config.DB.Offset(skip).Limit(limit).Find(&payments).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, payments) +} + +// GetPaymentByID retrieves a payment by ID +func GetPaymentByID(c *gin.Context) { + id := c.Param("id") + var payment models.Payment + + if err := config.DB.First(&payment, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Payment not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, payment) +} + +// CreatePayment creates a new payment +func CreatePayment(c *gin.Context) { + var input models.Payment + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Generate unique payment number + paymentNo := "PMT" + uuid.New().String()[:16] + + // Create payment + payment := models.Payment{ + PaymentNo: paymentNo, + OrderID: input.OrderID, + UserID: input.UserID, + Amount: input.Amount, + PaymentType: input.PaymentType, + PaymentMethod: input.PaymentMethod, + Status: input.Status, + TransactionID: input.TransactionID, + } + + if err := config.DB.Create(&payment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, payment) +} + +// UpdatePayment updates a payment's information +func UpdatePayment(c *gin.Context) { + id := c.Param("id") + var payment models.Payment + + // Check if payment exists + if err := config.DB.First(&payment, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Payment not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Parse input + var input models.Payment + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update payment fields + if input.OrderID != 0 { + payment.OrderID = input.OrderID + } + if input.UserID != 0 { + payment.UserID = input.UserID + } + if input.Amount != 0 { + payment.Amount = input.Amount + } + if input.PaymentType != "" { + payment.PaymentType = input.PaymentType + } + if input.PaymentMethod != "" { + payment.PaymentMethod = input.PaymentMethod + } + if input.Status != "" { + payment.Status = input.Status + } + if input.TransactionID != "" { + payment.TransactionID = input.TransactionID + } + + // Save updated payment + if err := config.DB.Save(&payment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, payment) +} + +// DeletePayment deletes a payment by ID +func DeletePayment(c *gin.Context) { + id := c.Param("id") + var payment models.Payment + + // Check if payment exists + if err := config.DB.First(&payment, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Payment not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Delete payment + if err := config.DB.Delete(&payment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, payment) +} \ No newline at end of file diff --git a/go-backend/controllers/user_controller.go b/go-backend/controllers/user_controller.go new file mode 100644 index 0000000..70b9e3d --- /dev/null +++ b/go-backend/controllers/user_controller.go @@ -0,0 +1,153 @@ +package controllers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "niumall-go/config" + "niumall-go/models" + "niumall-go/utils" +) + +// GetUserByID retrieves a user by ID +func GetUserByID(c *gin.Context) { + id := c.Param("id") + var user models.User + + if err := config.DB.First(&user, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, user) +} + +// GetUsers retrieves a list of users with pagination +func GetUsers(c *gin.Context) { + var users []models.User + skip, _ := strconv.Atoi(c.DefaultQuery("skip", "0")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100")) + + if err := config.DB.Offset(skip).Limit(limit).Find(&users).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, users) +} + +// UpdateUser updates a user's information +func UpdateUser(c *gin.Context) { + id := c.Param("id") + var user models.User + + // Check if user exists + if err := config.DB.First(&user, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Parse input + var input models.User + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update user fields + if input.Username != "" { + user.Username = input.Username + } + if input.UserType != "" { + user.UserType = input.UserType + } + if input.Status != "" { + user.Status = input.Status + } + + // Save updated user + if err := config.DB.Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, user) +} + +// DeleteUser deletes a user by ID +func DeleteUser(c *gin.Context) { + id := c.Param("id") + var user models.User + + // Check if user exists + if err := config.DB.First(&user, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Delete user + if err := config.DB.Delete(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, user) +} + +// Login handles user login and returns a JWT token +func Login(c *gin.Context) { + var input struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + } + + // Parse input + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Find user by username + var user models.User + if err := config.DB.Where("username = ?", input.Username).First(&user).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Check password + if !utils.CheckPasswordHash(input.Password, user.Password) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + + // Generate JWT token + token, err := utils.GenerateJWT(user.UUID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "access_token": token, + "token_type": "bearer", + }) +} \ No newline at end of file diff --git a/go-backend/docker-compose.yml b/go-backend/docker-compose.yml new file mode 100644 index 0000000..9a3fce6 --- /dev/null +++ b/go-backend/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + app: + build: . + ports: + - "8080:8080" + environment: + - DB_HOST=db + - DB_PORT=3306 + - DB_USER=root + - DB_PASSWORD=rootpassword + - DB_NAME=niumall + - PORT=8080 + - GIN_MODE=release + - JWT_SECRET=mysecretkey + depends_on: + - db + volumes: + - .env:/root/.env + + db: + image: mysql:8.0 + environment: + - MYSQL_ROOT_PASSWORD=rootpassword + - MYSQL_DATABASE=niumall + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + +volumes: + db_data: \ No newline at end of file diff --git a/go-backend/docs/orders.yaml b/go-backend/docs/orders.yaml new file mode 100644 index 0000000..fd5883c --- /dev/null +++ b/go-backend/docs/orders.yaml @@ -0,0 +1,412 @@ +openapi: 3.0.0 +info: + title: 订单管理API + description: 订单管理相关接口文档 + version: 1.0.0 + +paths: + /api/orders: + get: + summary: 获取订单列表 + description: 获取系统中的订单列表,支持分页(需要认证) + security: + - bearerAuth: [] + parameters: + - name: skip + in: query + description: 跳过的记录数 + required: false + schema: + type: integer + default: 0 + - name: limit + in: query + description: 返回的记录数 + required: false + schema: + type: integer + default: 100 + responses: + '200': + description: 成功返回订单列表 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Order' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: 创建订单 + description: 创建一个新的订单(需要认证) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderCreate' + responses: + '200': + description: 成功创建订单 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/orders/{id}: + get: + summary: 获取订单详情 + description: 根据订单ID获取订单详细信息(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 订单ID + required: true + schema: + type: integer + responses: + '200': + description: 成功返回订单信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 订单未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: 更新订单信息 + description: 根据订单ID更新订单信息(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 订单ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderUpdate' + responses: + '200': + description: 成功更新订单信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 订单未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + summary: 删除订单 + description: 根据订单ID删除订单(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 订单ID + required: true + schema: + type: integer + responses: + '200': + description: 成功删除订单 + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 订单未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + Order: + type: object + properties: + id: + type: integer + description: 订单ID + order_no: + type: string + description: 订单号 + buyer_id: + type: integer + description: 买家ID + seller_id: + type: integer + description: 卖家ID + variety_type: + type: string + description: 品种类型 + weight_range: + type: string + description: 重量范围 + weight_actual: + type: number + format: float + description: 实际重量 + price_per_unit: + type: number + format: float + description: 单价 + total_price: + type: number + format: float + description: 总价 + advance_payment: + type: number + format: float + description: 预付款 + final_payment: + type: number + format: float + description: 尾款 + status: + type: string + enum: [pending, confirmed, processing, shipped, delivered, cancelled, completed] + description: 订单状态 + delivery_address: + type: string + description: 收货地址 + delivery_time: + type: string + format: date-time + description: 交付时间 + remark: + type: string + description: 备注 + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - id + - order_no + - buyer_id + - seller_id + - variety_type + - weight_range + - price_per_unit + - total_price + - advance_payment + - final_payment + - status + - created_at + - updated_at + + OrderCreate: + type: object + properties: + buyer_id: + type: integer + description: 买家ID + seller_id: + type: integer + description: 卖家ID + variety_type: + type: string + description: 品种类型 + weight_range: + type: string + description: 重量范围 + weight_actual: + type: number + format: float + description: 实际重量 + price_per_unit: + type: number + format: float + description: 单价 + total_price: + type: number + format: float + description: 总价 + advance_payment: + type: number + format: float + description: 预付款 + final_payment: + type: number + format: float + description: 尾款 + status: + type: string + enum: [pending, confirmed, processing, shipped, delivered, cancelled, completed] + description: 订单状态 + delivery_address: + type: string + description: 收货地址 + delivery_time: + type: string + format: date-time + description: 交付时间 + remark: + type: string + description: 备注 + required: + - buyer_id + - seller_id + - variety_type + - weight_range + - price_per_unit + - total_price + + OrderUpdate: + type: object + properties: + buyer_id: + type: integer + description: 买家ID + seller_id: + type: integer + description: 卖家ID + variety_type: + type: string + description: 品种类型 + weight_range: + type: string + description: 重量范围 + weight_actual: + type: number + format: float + description: 实际重量 + price_per_unit: + type: number + format: float + description: 单价 + total_price: + type: number + format: float + description: 总价 + advance_payment: + type: number + format: float + description: 预付款 + final_payment: + type: number + format: float + description: 尾款 + status: + type: string + enum: [pending, confirmed, processing, shipped, delivered, cancelled, completed] + description: 订单状态 + delivery_address: + type: string + description: 收货地址 + delivery_time: + type: string + format: date-time + description: 交付时间 + remark: + type: string + description: 备注 + + Error: + type: object + properties: + error: + type: string + description: 错误信息 + required: + - error + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT \ No newline at end of file diff --git a/go-backend/docs/payments.yaml b/go-backend/docs/payments.yaml new file mode 100644 index 0000000..17f0d61 --- /dev/null +++ b/go-backend/docs/payments.yaml @@ -0,0 +1,355 @@ +openapi: 3.0.0 +info: + title: 支付管理API + description: 支付管理相关接口文档 + version: 1.0.0 + +paths: + /api/payments: + get: + summary: 获取支付列表 + description: 获取系统中的支付列表,支持分页(需要认证) + security: + - bearerAuth: [] + parameters: + - name: skip + in: query + description: 跳过的记录数 + required: false + schema: + type: integer + default: 0 + - name: limit + in: query + description: 返回的记录数 + required: false + schema: + type: integer + default: 100 + responses: + '200': + description: 成功返回支付列表 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Payment' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: 创建支付 + description: 创建一个新的支付(需要认证) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PaymentCreate' + responses: + '200': + description: 成功创建支付 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '400': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/payments/{id}: + get: + summary: 获取支付详情 + description: 根据支付ID获取支付详细信息(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 支付ID + required: true + schema: + type: integer + responses: + '200': + description: 成功返回支付信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 支付未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: 更新支付信息 + description: 根据支付ID更新支付信息(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 支付ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PaymentUpdate' + responses: + '200': + description: 成功更新支付信息 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '400': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 支付未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + summary: 删除支付 + description: 根据支付ID删除支付(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 支付ID + required: true + schema: + type: integer + responses: + '200': + description: 成功删除支付 + content: + application/json: + schema: + $ref: '#/components/schemas/Payment' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 支付未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + Payment: + type: object + properties: + id: + type: integer + description: 支付ID + payment_no: + type: string + description: 支付编号 + order_id: + type: integer + description: 关联订单ID + amount: + type: number + format: float + description: 支付金额 + payment_type: + type: string + enum: [advance, final] + description: 支付类型 + payment_method: + type: string + enum: [alipay, wechat, bank] + description: 支付方式 + status: + type: string + enum: [pending, paid, failed, refunded] + description: 支付状态 + transaction_id: + type: string + description: 第三方交易ID + paid_at: + type: string + format: date-time + description: 支付时间 + remark: + type: string + description: 备注 + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - id + - payment_no + - order_id + - amount + - payment_type + - payment_method + - status + - created_at + - updated_at + + PaymentCreate: + type: object + properties: + order_id: + type: integer + description: 关联订单ID + amount: + type: number + format: float + description: 支付金额 + payment_type: + type: string + enum: [advance, final] + description: 支付类型 + payment_method: + type: string + enum: [alipay, wechat, bank] + description: 支付方式 + status: + type: string + enum: [pending, paid, failed, refunded] + description: 支付状态 + transaction_id: + type: string + description: 第三方交易ID + paid_at: + type: string + format: date-time + description: 支付时间 + remark: + type: string + description: 备注 + required: + - order_id + - amount + - payment_type + - payment_method + + PaymentUpdate: + type: object + properties: + order_id: + type: integer + description: 关联订单ID + amount: + type: number + format: float + description: 支付金额 + payment_type: + type: string + enum: [advance, final] + description: 支付类型 + payment_method: + type: string + enum: [alipay, wechat, bank] + description: 支付方式 + status: + type: string + enum: [pending, paid, failed, refunded] + description: 支付状态 + transaction_id: + type: string + description: 第三方交易ID + paid_at: + type: string + format: date-time + description: 支付时间 + remark: + type: string + description: 备注 + + Error: + type: object + properties: + error: + type: string + description: 错误信息 + required: + - error + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT \ No newline at end of file diff --git a/go-backend/docs/users.yaml b/go-backend/docs/users.yaml new file mode 100644 index 0000000..520620a --- /dev/null +++ b/go-backend/docs/users.yaml @@ -0,0 +1,280 @@ +openapi: 3.0.0 +info: + title: 用户管理API + description: 用户管理相关接口文档 + version: 1.0.0 + +paths: + /api/login: + post: + summary: 用户登录 + description: 用户登录并获取JWT令牌 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: 用户名 + password: + type: string + description: 密码 + required: + - username + - password + responses: + '200': + description: 登录成功,返回访问令牌 + content: + application/json: + schema: + type: object + properties: + access_token: + type: string + description: JWT访问令牌 + token_type: + type: string + description: 令牌类型 + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 用户名或密码错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users: + get: + summary: 获取用户列表 + description: 获取系统中的用户列表,支持分页 + parameters: + - name: skip + in: query + description: 跳过的记录数 + required: false + schema: + type: integer + default: 0 + - name: limit + in: query + description: 返回的记录数 + required: false + schema: + type: integer + default: 100 + responses: + '200': + description: 成功返回用户列表 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/users/{id}: + get: + summary: 获取用户详情 + description: 根据用户ID获取用户详细信息 + parameters: + - name: id + in: path + description: 用户ID + required: true + schema: + type: integer + responses: + '200': + description: 成功返回用户信息 + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: 用户未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: 更新用户信息 + description: 根据用户ID更新用户信息(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 用户ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdate' + responses: + '200': + description: 成功更新用户信息 + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: 请求参数验证失败 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 用户未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + summary: 删除用户 + description: 根据用户ID删除用户(需要认证) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + description: 用户ID + required: true + schema: + type: integer + responses: + '200': + description: 成功删除用户 + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '401': + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 用户未找到 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + User: + type: object + properties: + id: + type: integer + description: 用户ID + uuid: + type: string + description: 用户UUID + username: + type: string + description: 用户名 + user_type: + type: string + enum: [client, supplier, driver, staff, admin] + description: 用户类型 + status: + type: string + enum: [active, inactive, locked] + description: 用户状态 + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - id + - uuid + - username + - user_type + - status + - created_at + - updated_at + + UserUpdate: + type: object + properties: + username: + type: string + description: 用户名 + user_type: + type: string + enum: [client, supplier, driver, staff, admin] + description: 用户类型 + status: + type: string + enum: [active, inactive, locked] + description: 用户状态 + + Error: + type: object + properties: + error: + type: string + description: 错误信息 + required: + - error + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT \ No newline at end of file diff --git a/go-backend/go.mod b/go-backend/go.mod new file mode 100644 index 0000000..a38291f --- /dev/null +++ b/go-backend/go.mod @@ -0,0 +1,39 @@ +module niumall-go + +go 1.19 + +require ( + github.com/gin-gonic/gin v1.9.1 + gorm.io/driver/mysql v1.5.1 + gorm.io/gorm v1.25.1 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/joho/godotenv v1.5.1 + github.com/go-playground/validator/v10 v10.14.0 + golang.org/x/crypto v0.10.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/go-backend/main.go b/go-backend/main.go new file mode 100644 index 0000000..ca3ebfb --- /dev/null +++ b/go-backend/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "log" + "os" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + + "niumall-go/config" + "niumall-go/routes" +) + +func main() { + // Load environment variables + if err := godotenv.Load(); err != nil { + log.Println("No .env file found") + } + + // Initialize database + config.InitDB() + + // Set Gin to release mode in production + if os.Getenv("GIN_MODE") == "release" { + gin.SetMode(gin.ReleaseMode) + } + + // Create Gin router + router := gin.Default() + + // Register routes + routes.RegisterRoutes(router) + + // Get port from environment variable or use default + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + // Start server + log.Printf("Server starting on port %s", port) + if err := router.Run(":" + port); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} \ No newline at end of file diff --git a/go-backend/middleware/auth.go b/go-backend/middleware/auth.go new file mode 100644 index 0000000..7382fd9 --- /dev/null +++ b/go-backend/middleware/auth.go @@ -0,0 +1,45 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "niumall-go/utils" +) + +// AuthMiddleware authenticates requests using JWT +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Get authorization header + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + c.Abort() + return + } + + // Check if header has Bearer prefix + if !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"}) + c.Abort() + return + } + + // Extract token + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + // Parse and validate token + claims, err := utils.ParseJWT(tokenString) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) + c.Abort() + return + } + + // Set user UUID in context for use in handlers + c.Set("user_uuid", claims.UUID) + c.Next() + } +} \ No newline at end of file diff --git a/go-backend/models/order.go b/go-backend/models/order.go new file mode 100644 index 0000000..b2d3492 --- /dev/null +++ b/go-backend/models/order.go @@ -0,0 +1,44 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type OrderStatus string + +const ( + OrderStatusPending OrderStatus = "pending" + OrderStatusConfirmed OrderStatus = "confirmed" + OrderStatusProcessing OrderStatus = "processing" + OrderStatusShipped OrderStatus = "shipped" + OrderStatusDelivered OrderStatus = "delivered" + OrderStatusCancelled OrderStatus = "cancelled" + OrderStatusCompleted OrderStatus = "completed" +) + +type Order struct { + ID uint `gorm:"primaryKey" json:"id"` + OrderNo string `gorm:"uniqueIndex;not null" json:"order_no"` + BuyerID uint `gorm:"not null" json:"buyer_id"` + SellerID uint `gorm:"not null" json:"seller_id"` + VarietyType string `gorm:"not null" json:"variety_type"` + WeightRange string `gorm:"not null" json:"weight_range"` + WeightActual float64 `json:"weight_actual"` + PricePerUnit float64 `gorm:"not null" json:"price_per_unit"` + TotalPrice float64 `gorm:"not null" json:"total_price"` + AdvancePayment float64 `gorm:"default:0.0" json:"advance_payment"` + FinalPayment float64 `gorm:"default:0.0" json:"final_payment"` + Status OrderStatus `gorm:"default:pending" json:"status"` + DeliveryAddress string `json:"delivery_address"` + DeliveryTime *time.Time `json:"delivery_time"` + Remark string `json:"remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (Order) TableName() string { + return "orders" +} \ No newline at end of file diff --git a/go-backend/models/payment.go b/go-backend/models/payment.go new file mode 100644 index 0000000..963d34a --- /dev/null +++ b/go-backend/models/payment.go @@ -0,0 +1,44 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type PaymentType string +type PaymentMethod string +type PaymentStatus string + +const ( + PaymentTypeAdvance PaymentType = "advance" + PaymentTypeFinal PaymentType = "final" + + PaymentMethodWechat PaymentMethod = "wechat" + PaymentMethodAlipay PaymentMethod = "alipay" + PaymentMethodBank PaymentMethod = "bank" + + PaymentStatusPending PaymentStatus = "pending" + PaymentStatusSuccess PaymentStatus = "success" + PaymentStatusFailed PaymentStatus = "failed" + PaymentStatusRefunded PaymentStatus = "refunded" +) + +type Payment struct { + ID uint `gorm:"primaryKey" json:"id"` + PaymentNo string `gorm:"uniqueIndex;not null" json:"payment_no"` + OrderID uint `gorm:"not null" json:"order_id"` + UserID uint `gorm:"not null" json:"user_id"` + Amount float64 `gorm:"not null" json:"amount"` + PaymentType PaymentType `gorm:"not null" json:"payment_type"` + PaymentMethod PaymentMethod `gorm:"not null" json:"payment_method"` + Status PaymentStatus `gorm:"default:pending" json:"status"` + TransactionID string `json:"transaction_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (Payment) TableName() string { + return "payments" +} \ No newline at end of file diff --git a/go-backend/models/user.go b/go-backend/models/user.go new file mode 100644 index 0000000..5747ca6 --- /dev/null +++ b/go-backend/models/user.go @@ -0,0 +1,38 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type UserType string +type UserStatus string + +const ( + UserTypeClient UserType = "client" + UserTypeSupplier UserType = "supplier" + UserTypeDriver UserType = "driver" + UserTypeStaff UserType = "staff" + UserTypeAdmin UserType = "admin" + + UserStatusActive UserStatus = "active" + UserStatusInactive UserStatus = "inactive" + UserStatusLocked UserStatus = "locked" +) + +type User struct { + ID uint `gorm:"primaryKey" json:"id"` + UUID string `gorm:"uniqueIndex;not null" json:"uuid"` + Username string `gorm:"not null" json:"username"` + Password string `gorm:"not null" json:"-"` + UserType UserType `gorm:"default:client" json:"user_type"` + Status UserStatus `gorm:"default:active" json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (User) TableName() string { + return "users" +} \ No newline at end of file diff --git a/go-backend/routes/routes.go b/go-backend/routes/routes.go new file mode 100644 index 0000000..d70000d --- /dev/null +++ b/go-backend/routes/routes.go @@ -0,0 +1,44 @@ +package routes + +import ( + "github.com/gin-gonic/gin" + + "niumall-go/controllers" + "niumall-go/middleware" +) + +func RegisterRoutes(router *gin.Engine) { + // Public routes + public := router.Group("/api") + { + // User authentication + public.POST("/login", controllers.Login) + + // Public user routes + public.GET("/users", controllers.GetUsers) + public.GET("/users/:id", controllers.GetUserByID) + } + + // Protected routes + protected := router.Group("/api") + protected.Use(middleware.AuthMiddleware()) + { + // User routes + protected.PUT("/users/:id", controllers.UpdateUser) + protected.DELETE("/users/:id", controllers.DeleteUser) + + // Order routes + protected.GET("/orders", controllers.GetOrders) + protected.GET("/orders/:id", controllers.GetOrderByID) + protected.POST("/orders", controllers.CreateOrder) + protected.PUT("/orders/:id", controllers.UpdateOrder) + protected.DELETE("/orders/:id", controllers.DeleteOrder) + + // Payment routes + protected.GET("/payments", controllers.GetPayments) + protected.GET("/payments/:id", controllers.GetPaymentByID) + protected.POST("/payments", controllers.CreatePayment) + protected.PUT("/payments/:id", controllers.UpdatePayment) + protected.DELETE("/payments/:id", controllers.DeletePayment) + } +} \ No newline at end of file diff --git a/go-backend/utils/jwt.go b/go-backend/utils/jwt.go new file mode 100644 index 0000000..392325e --- /dev/null +++ b/go-backend/utils/jwt.go @@ -0,0 +1,62 @@ +package utils + +import ( + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var jwtSecret = []byte(os.Getenv("JWT_SECRET")) + +type Claims struct { + UUID string `json:"uuid"` + jwt.RegisteredClaims +} + +// GenerateJWT generates a new JWT token for a user +func GenerateJWT(uuid string) (string, error) { + // Set expiration time (24 hours from now) + expirationTime := time.Now().Add(24 * time.Hour) + + // Create claims + claims := &Claims{ + UUID: uuid, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + // Create token with claims + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token + tokenString, err := token.SignedString(jwtSecret) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// ParseJWT parses and validates a JWT token +func ParseJWT(tokenString string) (*Claims, error) { + claims := &Claims{} + + // Parse token + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + // Check if token is valid + if !token.Valid { + return nil, jwt.ErrTokenMalformed + } + + return claims, nil +} \ No newline at end of file diff --git a/go-backend/utils/password.go b/go-backend/utils/password.go new file mode 100644 index 0000000..1c2da0d --- /dev/null +++ b/go-backend/utils/password.go @@ -0,0 +1,17 @@ +package utils + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashPassword generates a bcrypt hash of the password +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPasswordHash compares a bcrypt hashed password with its possible plaintext equivalent +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} \ No newline at end of file