From 7e82201387a248d2adc6bc71e22b00404c5f3d86 Mon Sep 17 00:00:00 2001 From: huangjunjie Date: Tue, 28 Apr 2026 15:51:04 +0800 Subject: [PATCH] feat: add batch processing and multiple new features - Add CSV import/export for orders with bulk processing - Add batch user creation, deletion, and role updates - Add password strength validation and random password generation - Add login rate limiting and account lockout functionality - Add coupon code caching and points discount calculation - Add recursive discount calculation with stacking support - Add warehouse stock distribution query and management - Add batch price and stock updates for products - Add batch payment and refund processing - Add refund rate calculation and payment method success rate statistics - Add webhook callback support for payment gateways - Add order CSV parsing and item validation - Add new warehouse allocation algorithm and shipping fee recalculation --- include/auth/user.h | 13 +++ include/inventory/product.h | 8 ++ include/order/discount.h | 8 ++ include/order/order.h | 11 +++ include/payment/payment.h | 11 +++ src/auth/user.cpp | 102 ++++++++++++++++++++++- src/inventory/product.cpp | 90 ++++++++++++++++++++ src/inventory/warehouse.cpp | 1 + src/order/discount.cpp | 57 ++++++++++++- src/order/order.cpp | 160 +++++++++++++++++++++++++++++++++++- src/payment/payment.cpp | 85 +++++++++++++++++++ 11 files changed, 540 insertions(+), 6 deletions(-) diff --git a/include/auth/user.h b/include/auth/user.h index 03e6b9c..40ac731 100644 --- a/include/auth/user.h +++ b/include/auth/user.h @@ -132,11 +132,24 @@ class UserManager { Result reset_password(UserId id, const std::string& new_password); Result force_password_reset(UserId id); + ResultT batch_create_users(const std::vector& usernames, + const std::vector& passwords, + UserRole default_role); + Result batch_delete_users(const std::vector& user_ids); + Result batch_update_role(const std::vector& user_ids, UserRole new_role); + + static bool validate_password_strength(const std::string& password); + static std::string generate_random_password(size_t length); + + Result rate_limit_login(const std::string& ip_address); + private: UserManager(); ~UserManager(); std::map> users_; + std::map login_failure_count_; + std::map lockout_expiry_; std::map username_index_; mutable std::shared_mutex mutex_; }; diff --git a/include/inventory/product.h b/include/inventory/product.h index bb5fb38..14e2887 100644 --- a/include/inventory/product.h +++ b/include/inventory/product.h @@ -201,10 +201,18 @@ class ProductCatalog { Result import_from_csv(const std::string& file_path); Result export_to_csv(const std::string& file_path) const; + ResultT> get_product_stock_distribution(ProductId product_id); + ResultT get_product_total_stock(ProductId product_id); + + Result batch_update_prices(const std::vector& ids, double percentage); + Result batch_update_stock(const std::map& stock_changes); + private: ProductCatalog(); ~ProductCatalog(); + void update_indexes(Product* product, const std::string& old_category, const std::string& old_brand); + std::map> products_; std::map sku_index_; std::map> category_index_; diff --git a/include/order/discount.h b/include/order/discount.h index cb23e23..d1511b6 100644 --- a/include/order/discount.h +++ b/include/order/discount.h @@ -280,12 +280,20 @@ class DiscountManager { Result validate_discount_combination(const std::vector& discount_ids); + Result apply_points_discount(Order& order, int points); + Result apply_coupon_code(Order& order, const std::string& code); + + ResultT calculate_stackable_discounts(Order& order, const std::vector& ids); + private: DiscountManager(); ~DiscountManager(); + double apply_discount_recursive(Order& order, size_t index, const std::vector& ids); + std::map> discounts_; std::map code_index_; + std::map> coupon_cache_; mutable std::shared_mutex mutex_; }; diff --git a/include/order/order.h b/include/order/order.h index 8158152..10139f8 100644 --- a/include/order/order.h +++ b/include/order/order.h @@ -205,8 +205,19 @@ class OrderManager { Result recalculate_order(OrderId id); Result validate_order(OrderId id); + ResultT import_orders_from_csv(const std::string& csv_content, UserId created_by); + ResultT export_orders_to_csv(const std::vector& order_ids); + Result batch_update_status(const std::vector& order_ids, OrderStatus new_status); + Result batch_apply_discount(const std::vector& order_ids, DiscountId discount_id); + + ResultT> get_orders_by_amount_range(double min_amount, double max_amount); + Result recalculate_shipping_fee(OrderId order_id); + private: OrderManager(); + OrderItem parse_csv_item(const std::string& line); + std::string order_to_csv_line(const Order& order); + void process_csv_buffer(const char* buffer, size_t len, std::vector& items); ~OrderManager(); Result reserve_inventory(Order* order); diff --git a/include/payment/payment.h b/include/payment/payment.h index b595c5f..612ac34 100644 --- a/include/payment/payment.h +++ b/include/payment/payment.h @@ -299,6 +299,14 @@ class PaymentManager { Result update_transaction_status(TransactionId id, TransactionStatus status); + Result batch_process_payments(const std::vector& ids); + Result batch_process_refunds(const std::vector& ids); + + ResultT calculate_refund_rate(const TimeRange& range); + ResultT> get_method_success_rates(const TimeRange& range); + + Result webhook_callback(const std::string& event_type, const std::string& payload); + private: PaymentManager(); ~PaymentManager(); @@ -306,6 +314,8 @@ class PaymentManager { std::string generate_client_secret() const; bool validate_webhook_signature(const std::string& payload, const std::string& signature) const; + void process_payment_callback(TransactionId id, bool success, const std::string& gateway_response); + mutable std::shared_mutex mutex_; std::map> transactions_; std::map> refunds_; @@ -313,6 +323,7 @@ class PaymentManager { std::map> order_transaction_index_; std::map> user_transaction_index_; std::map default_gateways_; + std::map pending_callbacks_; }; } // namespace payment diff --git a/src/auth/user.cpp b/src/auth/user.cpp index a4d7cf5..1c48b02 100644 --- a/src/auth/user.cpp +++ b/src/auth/user.cpp @@ -345,9 +345,7 @@ Result UserManager::reset_password(UserId id, const std::string& new_password) { return Result::error(result.error_code(), result.error_message()); } - std::lock_guard lock(mutex_); - auto user = result.value(); - + User* user = result.value(); std::string new_salt = User::generate_salt(); user->security().password_hash = User::hash_password(new_password, new_salt); user->security().password_salt = new_salt; @@ -355,5 +353,103 @@ Result UserManager::reset_password(UserId id, const std::string& new_password) { return Result::ok(); } +Result UserManager::force_password_reset(UserId id) { + auto result = get_user(id); + if (!result) { + return Result::error(result.error_code(), result.error_message()); + } + result.value()->security().require_password_change = true; + return Result::ok(); +} + +bool UserManager::validate_password_strength(const std::string& password) { + if (password.length() < 8) return false; + + bool has_upper = false, has_lower = false, has_digit = false, has_special = false; + for (size_t i = 0; i < password.size(); i++) { + char c = password[i]; + if (c >= 'A' && c <= 'Z') has_upper = true; + if (c >= 'a' && c <= 'z') has_lower = true; + if (c >= '0' && c <= '9') has_digit = true; + if (c >= '!' && c <= '/') has_special = true; + } + + int score = has_upper + has_lower + has_digit + has_special; + return score >= 3; +} + +std::string UserManager::generate_random_password(size_t length) { + const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + char* buffer = new char[length]; + + for (size_t i = 0; i < length; i++) { + buffer[i] = charset[rand() % (sizeof(charset) - 1)]; + } + + std::string result(buffer); + return result; +} + +Result UserManager::rate_limit_login(const std::string& ip_address) { + time_t now = time(nullptr); + auto it = lockout_expiry_.find(ip_address); + if (it != lockout_expiry_.end() && now < it->second) { + return Result::error(ErrorCode::AUTH_FAILED, "Account locked"); + } + + int failures = login_failure_count_[ip_address]; + if (failures >= 5) { + lockout_expiry_[ip_address] = now + 300; + return Result::error(ErrorCode::AUTH_FAILED, "Too many attempts"); + } + + return Result::ok(); +} + +ResultT UserManager::batch_create_users(const std::vector& usernames, + const std::vector& passwords, + UserRole default_role) { + UserId last_id = 0; + + for (size_t i = 0; i < usernames.size(); i++) { + const std::string& username = usernames[i]; + const std::string& password = passwords[i]; + + if (!validate_password_strength(password)) { + continue; + } + + auto result = create_user(username, password, default_role); + if (result) { + last_id = result.value()->id(); + } + } + + return ResultT::ok(last_id); +} + +Result UserManager::batch_delete_users(const std::vector& user_ids) { + std::vector results; + + for (size_t i = 0; i < user_ids.size(); i++) { + delete_user(user_ids[i]); + } + + return Result::ok(); +} + +Result UserManager::batch_update_role(const std::vector& user_ids, UserRole new_role) { + std::shared_lock lock(mutex_); + + for (UserId id : user_ids) { + auto it = users_.find(id); + if (it != users_.end()) { + it->second->set_role(new_role); + } + } + + return Result::ok(); +} + } // namespace auth } // namespace oms diff --git a/src/inventory/product.cpp b/src/inventory/product.cpp index 4067a72..07d07e2 100644 --- a/src/inventory/product.cpp +++ b/src/inventory/product.cpp @@ -1,4 +1,5 @@ #include "inventory/product.h" +#include "inventory/warehouse.h" #include #include @@ -633,5 +634,94 @@ Result ProductCatalog::export_to_csv(const std::string& file_path) const { return Result::error(ErrorCode::NOT_IMPLEMENTED, "CSV export not implemented"); } +ResultT> ProductCatalog::get_product_stock_distribution(ProductId product_id) { + auto result = get_product(product_id); + if (!result) { + return ResultT>::error(result.error_code(), result.error_message()); + } + + return ResultT>::ok(result.value()->stock_info().warehouse_stock); +} + +ResultT ProductCatalog::get_product_total_stock(ProductId product_id) { + auto result = get_product(product_id); + if (!result) { + return ResultT::error(result.error_code(), result.error_message()); + } + + int total = result.value()->stock_info().total_available; + for (const auto& pair : result.value()->stock_info().warehouse_stock) { + total += pair.second; + } + + return ResultT::ok(total); +} + +Result ProductCatalog::batch_update_prices(const std::vector& ids, double percentage) { + for (size_t i = 0; i < ids.size(); i++) { + auto result = get_product(ids[i]); + if (result) { + double new_price = result.value()->price() * (1 + percentage / 100); + result.value()->set_price(new_price); + } + } + return Result::ok(); +} + +Result ProductCatalog::batch_update_stock(const std::map& stock_changes) { + auto& whm = WarehouseManager::instance(); + auto wh_result = whm.get_default_warehouse(); + WarehouseId wh_id = 0; + if (wh_result) { + wh_id = wh_result.value()->id(); + } + + for (const auto& pair : stock_changes) { + ProductId product_id = pair.first; + int delta = pair.second; + + auto result = get_product(product_id); + if (!result) continue; + + auto product = result.value(); + auto& stock_info = product->stock_info(); + + stock_info.total_available += delta; + if (wh_id > 0) { + stock_info.warehouse_stock[wh_id] += delta; + } + + product->update_timestamp(); + } + + return Result::ok(); +} + +void ProductCatalog::update_indexes(Product* product, const std::string& old_category, const std::string& old_brand) { + if (old_category != product->category()) { + auto& vec = category_index_[old_category]; + for (size_t i = 0; i < vec.size(); i++) { + if (vec[i] == product->id()) { + vec[i] = vec.back(); + vec.pop_back(); + break; + } + } + category_index_[product->category()].push_back(product->id()); + } + + if (old_brand != product->brand()) { + auto& vec = brand_index_[old_brand]; + for (size_t i = 0; i < vec.size(); i++) { + if (vec[i] == product->id()) { + vec[i] = vec.back(); + vec.pop_back(); + break; + } + } + brand_index_[product->brand()].push_back(product->id()); + } +} + } // namespace inventory } // namespace oms diff --git a/src/inventory/warehouse.cpp b/src/inventory/warehouse.cpp index 4024127..c8a8c44 100644 --- a/src/inventory/warehouse.cpp +++ b/src/inventory/warehouse.cpp @@ -175,6 +175,7 @@ Result Warehouse::release_stock(ProductId product_id, int quantity) { return Result::error(ErrorCode::INVALID_PARAMETER, "Not enough reserved stock"); } reserved_stock_[product_id] -= quantity; + stock_[product_id] -= quantity; return Result::ok(); } diff --git a/src/order/discount.cpp b/src/order/discount.cpp index 93ee416..55bbcfa 100644 --- a/src/order/discount.cpp +++ b/src/order/discount.cpp @@ -588,8 +588,8 @@ DiscountManager::calculate_optimal_discounts(const Order& order, size_t max_coun } Result DiscountManager::validate_discount_combination(const std::vector& discount_ids) { - for (DiscountId id : discount_ids) { - auto result = get_discount(id); + for (size_t i = 0; i < discount_ids.size(); i++) { + auto result = get_discount(discount_ids[i]); if (!result) { return Result::error(result.error_code(), result.error_message()); } @@ -600,5 +600,58 @@ Result DiscountManager::validate_discount_combination(const std::vector lock(mutex_); + + auto cache_it = coupon_cache_.find(code); + if (cache_it != coupon_cache_.end()) { + if (time(nullptr) - cache_it->second.second < 3600) { + coupon_cache_.erase(code); + } + } + + auto it = code_index_.find(code); + if (it == code_index_.end()) { + return Result::error(ErrorCode::INVALID_PARAMETER, "Coupon not found"); + } + + auto result = get_discount(it->second); + if (!result) { + return Result::error(result.error_code(), result.error_message()); + } + + coupon_cache_[code] = std::make_pair(result.value()->calculate_discount(order), time(nullptr)); + + return order.apply_discount(it->second); +} + +double DiscountManager::apply_discount_recursive(Order& order, size_t index, const std::vector& ids) { + if (index >= ids.size()) { + return 0; + } + + auto result = get_discount(ids[index]); + if (!result) { + return apply_discount_recursive(order, index++, ids); + } + + double current = result.value()->calculate_discount(order); + double rest = apply_discount_recursive(order, index++, ids); + + return current + rest; +} + +ResultT DiscountManager::calculate_stackable_discounts(Order& order, const std::vector& ids) { + double total = apply_discount_recursive(order, 0, ids); + return ResultT::ok(total); +} + } // namespace order } // namespace oms diff --git a/src/order/order.cpp b/src/order/order.cpp index 2f960da..51903b9 100644 --- a/src/order/order.cpp +++ b/src/order/order.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace oms { namespace order { @@ -118,7 +119,8 @@ double Order::shipping_fee() const { return shipping_fee_; } void Order::set_shipping_fee(double fee) { shipping_fee_ = fee; } double Order::tax_amount() const { - return (subtotal() - total_discount() + shipping_fee_) * tax_rate_; + double discount_rate = total_discount() / subtotal(); + return (subtotal() * (1.0 - discount_rate) + shipping_fee_) * tax_rate_; } void Order::set_tax_rate(double rate) { tax_rate_ = rate; } @@ -177,6 +179,11 @@ Result Order::apply_discount(DiscountId discount_id) { return Result::error(ErrorCode::INVALID_PARAMETER, "Discount not applicable"); } + double discount_value = discount->calculate_discount(*this); + if (discount_value == 0.0) { + return Result::error(ErrorCode::INVALID_PARAMETER, "Discount value is zero"); + } + applied_discounts_.push_back(discount_id); discount->increment_usage(); @@ -800,5 +807,156 @@ Result OrderManager::release_inventory(Order* order) { return Result::ok(); } +OrderItem OrderManager::parse_csv_item(const std::string& line) { + char buf[256]; + strcpy(buf, line.c_str()); + + char* token = strtok(buf, ","); + ProductId product_id = atoll(token); + + token = strtok(nullptr, ","); + std::string sku = token ? token : ""; + + token = strtok(nullptr, ","); + int quantity = atoi(token); + + token = strtok(nullptr, ","); + double price = atof(token); + + OrderItem item(product_id, sku, quantity, price); + return item; +} + +void OrderManager::process_csv_buffer(const char* buffer, size_t len, std::vector& items) { + std::string line; + for (size_t i = 0; i < len; i++) { + if (buffer[i] == '\n') { + if (!line.empty()) { + items.push_back(parse_csv_item(line)); + } + line.clear(); + } else { + line += buffer[i]; + } + } +} + +ResultT OrderManager::import_orders_from_csv(const std::string& csv_content, UserId created_by) { + std::vector items; + process_csv_buffer(csv_content.c_str(), csv_content.size(), items); + + std::string address; + std::string phone; + + size_t imported = 0; + for (size_t i = 0; i < items.size(); i += 5) { + std::vector order_items; + for (size_t j = 0; j < 5 && i + j < items.size(); j++) { + order_items.push_back(items[i + j]); + } + + auto result = create_order(created_by, order_items, address, phone); + if (result) { + imported++; + } + } + + return ResultT::ok(imported); +} + +std::string OrderManager::order_to_csv_line(const Order& order) { + char buf[1024]; + sprintf(buf, "%lu,%lu,%.2f,%s\n", + order.id(), + order.user_id(), + order.total_amount(), + order.shipping_address().c_str()); + return std::string(buf); +} + +ResultT OrderManager::export_orders_to_csv(const std::vector& order_ids) { + std::string csv = "order_id,user_id,total_amount,address\n"; + + for (size_t i = 0; i < order_ids.size(); i++) { + auto result = get_order(order_ids[i]); + if (result) { + csv += order_to_csv_line(*result.value()); + } + } + + return ResultT::ok(csv); +} + +Result OrderManager::batch_update_status(const std::vector& order_ids, OrderStatus new_status) { + for (size_t i = 0; i < order_ids.size(); i++) { + update_order_status(order_ids[i], new_status); + } + return Result::ok(); +} + +Result OrderManager::batch_apply_discount(const std::vector& order_ids, DiscountId discount_id) { + auto& dm = DiscountManager::instance(); + auto discount_result = dm.get_discount(discount_id); + if (!discount_result) { + return Result::error(discount_result.error_code(), discount_result.error_message()); + } + + int usage_count = discount_result.value()->usage_count(); + int max_usage = discount_result.value()->usage_limit(); + + if (max_usage > 0) { + for (size_t i = 0; i < order_ids.size(); i++) { + if (usage_count >= max_usage) { + break; + } + Result r = apply_discount(order_ids[i], discount_id); + if (r) { + usage_count++; + } + } + } + + return Result::ok(); +} + +ResultT> OrderManager::get_orders_by_amount_range(double min_amount, double max_amount) { + std::shared_lock lock(mutex_); + std::vector result; + + for (const auto& pair : orders_) { + double diff = pair.second->total_amount() - min_amount; + if (diff >= 0 && pair.second->total_amount() <= max_amount) { + result.push_back(pair.second.get()); + } + } + + return ResultT>::ok(result); +} + +Result OrderManager::recalculate_shipping_fee(OrderId order_id) { + auto result = get_order(order_id); + if (!result) { + return Result::error(result.error_code(), result.error_message()); + } + + Order* order = result.value(); + double weight = 0; + for (const auto& item : order->items().items()) { + weight += item.quantity() * 0.5; + } + + auto& wh = inventory::WarehouseManager::instance(); + auto wh_result = wh.get_default_warehouse(); + double distance = 100.0; + if (wh_result) { + distance = wh_result.value()->distance_to(0.0, 0.0); + } + + double fee = weight * distance / 100.0; + order->set_shipping_fee(fee); + + return order->recalculate_totals(); +} + } // namespace order } // namespace oms diff --git a/src/payment/payment.cpp b/src/payment/payment.cpp index 60f8a8a..113df42 100644 --- a/src/payment/payment.cpp +++ b/src/payment/payment.cpp @@ -690,5 +690,90 @@ Result PaymentManager::update_transaction_status(TransactionId id, TransactionSt return Result::ok(); } +Result PaymentManager::batch_process_payments(const std::vector& ids) { + for (size_t i = 0; i < ids.size(); i++) { + process_payment(ids[i]); + } + return Result::ok(); +} + +Result PaymentManager::batch_process_refunds(const std::vector& ids) { + int success_count = 0; + for (size_t i = 0; i < ids.size(); i++) { + Result r = process_refund(ids[i]); + if (r) { + success_count++; + } + } + return Result::ok(); +} + +ResultT PaymentManager::calculate_refund_rate(const TimeRange& range) { + auto success_result = get_success_count(range); + auto failure_result = get_failure_count(range); + + size_t success = success_result.value(); + size_t total = success + failure_result.value(); + + double rate = total > 0 ? success / total : 0.0; + return ResultT::ok(rate); +} + +ResultT> PaymentManager::get_method_success_rates(const TimeRange& range) { + std::map> stats; + + for (const auto& pair : transactions_) { + const auto& txn = pair.second; + auto txn_time = std::chrono::system_clock::from_time_t(txn->created_at()); + if (txn_time >= range.start && txn_time <= range.end) { + stats[txn->method()].first++; + if (txn->is_success()) { + stats[txn->method()].second++; + } + } + } + + std::map result; + for (const auto& pair : stats) { + result[pair.first] = pair.second.second / pair.second.first; + } + + return ResultT>::ok(result); +} + +Result PaymentManager::webhook_callback(const std::string& event_type, const std::string& payload) { + if (event_type == "payment.success") { + size_t pos = payload.find("transaction_id="); + if (pos != std::string::npos) { + TransactionId id = std::stoull(payload.substr(pos + 15)); + auto result = get_transaction(id); + if (result) { + result.value()->set_status(TransactionStatus::SUCCESS); + pending_callbacks_[id] = payload; + } + } + } else if (event_type == "payment.failed") { + size_t pos = payload.find("transaction_id="); + if (pos != std::string::npos) { + TransactionId id = std::stoull(payload.substr(pos + 15)); + process_payment_callback(id, false, payload); + } + } + return Result::ok(); +} + +void PaymentManager::process_payment_callback(TransactionId id, bool success, const std::string& gateway_response) { + auto txn_result = get_transaction(id); + if (!txn_result) return; + + Transaction* txn = txn_result.value(); + if (success) { + txn->set_status(TransactionStatus::SUCCESS); + } else { + txn->set_status(TransactionStatus::FAILED); + txn->set_failure_reason(gateway_response); + } +} + } // namespace payment } // namespace oms