增加Shop API
This commit is contained in:
1 parent
3769193696
commit
5d017740c6
83 files changed
+4451
-696
No files matched your search
@@ -0,0 +1,330 @@
|
||||
using System.Text.Json;
|
||||
using MAX.Framework.Conventions;
|
||||
using MAX.Framework.Dtos;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Services;
|
||||
|
||||
namespace Store.Shops.Controllers;
|
||||
|
||||
/// <summary>统一店铺接口:微信、拼多多、微店及相关物流业务;使用当前认证、JSON DTO和ApiResult响应。</summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[DefaultHttpPost]
|
||||
[Route("v{version:apiVersion}/shop/[action]")]
|
||||
public sealed class ShopController(ShopService shops, ShopManagementService pddShops) : ControllerBase
|
||||
{
|
||||
/// <summary>查询本人或直属主账号分配的微信店铺。</summary>
|
||||
[EndpointSummary("查询本人或直属主账号分配的微信店铺")]
|
||||
public Task<ShopPage<ShopDetailsInfo>> GetShops(ShopQueryRequest request, CancellationToken ct)
|
||||
=> shops.Search(request, ct: ct);
|
||||
|
||||
/// <summary>查询授权店铺的ERP商户余额。</summary>
|
||||
[EndpointSummary("查询授权店铺的ERP商户余额")]
|
||||
public Task<ShopPage<ErpAccountInfo>> GetErpMerchants(ErpRequest request, CancellationToken ct)
|
||||
=> shops.ErpMerchants(request, ct);
|
||||
|
||||
/// <summary>查询登录名下的授权店铺。</summary>
|
||||
[EndpointSummary("查询登录名下的授权店铺")]
|
||||
public Task<IReadOnlyList<ShopDetailsInfo>> GetShopsByLoginName(LoginRequest request, CancellationToken ct)
|
||||
=> shops.ByLogin(request, ct);
|
||||
|
||||
/// <summary>批量读取授权店铺Cookie。</summary>
|
||||
[EndpointSummary("批量读取授权店铺Cookie")]
|
||||
public Task<IReadOnlyList<ShopCookieInfo>> GetShopCookies(ShopIdsRequest request, CancellationToken ct)
|
||||
=> shops.Cookies(request, ct);
|
||||
|
||||
/// <summary>主账号新增微信店铺,重复记录拒绝创建。</summary>
|
||||
[EndpointSummary("主账号新增微信店铺,重复记录拒绝创建")]
|
||||
public async Task<ApiResult> AddShop(ShopSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.Save(request, true, false, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>删除本人所属的店铺记录。</summary>
|
||||
[EndpointSummary("删除本人所属的店铺记录")]
|
||||
public async Task<ApiResult> DeleteShop(LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.Delete(request, false, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>部分更新授权微信店铺。</summary>
|
||||
[EndpointSummary("部分更新授权微信店铺")]
|
||||
public async Task<ApiResult> UpdateShop(ShopSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.Save(request, false, false, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>按登录名刷新授权店铺Cookie与状态。</summary>
|
||||
[EndpointSummary("按登录名刷新授权店铺Cookie与状态")]
|
||||
public async Task<ApiResult> UpdateShopLogin(CookieRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.UpdateCookies(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>管理员分页查询全部店铺。</summary>
|
||||
[EndpointSummary("管理员分页查询全部店铺")]
|
||||
public Task<ShopPage<ShopDetailsInfo>> GetAllShops(ShopQueryRequest request, CancellationToken ct)
|
||||
=> shops.Search(request, admin: true, ct: ct);
|
||||
|
||||
/// <summary>管理员按选品操作员查询店铺并刷新月度额度。</summary>
|
||||
[EndpointSummary("管理员按选品操作员查询店铺并刷新月度额度")]
|
||||
public Task<ShopPage<ShopDetailsInfo>> GetSelectionShops(ShopQueryRequest request, CancellationToken ct)
|
||||
=> shops.Search(request, selection: true, ct: ct);
|
||||
|
||||
/// <summary>管理员批量设置店铺上传剩余额度。</summary>
|
||||
[EndpointSummary("管理员批量设置店铺上传剩余额度")]
|
||||
public async Task<ApiResult> UpdateShopQuota(QuotaRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.UpdateQuota(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>管理员修改指定账号的店铺。</summary>
|
||||
[EndpointSummary("管理员修改指定账号的店铺")]
|
||||
public async Task<ApiResult> AdminUpdateShop(ShopSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.Save(request, false, true, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>管理员为指定账号新增店铺。</summary>
|
||||
[EndpointSummary("管理员为指定账号新增店铺")]
|
||||
public async Task<ApiResult> AdminAddShop(ShopSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.Save(request, true, true, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>管理员删除指定账号店铺。</summary>
|
||||
[EndpointSummary("管理员删除指定账号店铺")]
|
||||
public async Task<ApiResult> AdminDeleteShop(LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.Delete(request, true, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>管理员批量分配或解除选品操作员。</summary>
|
||||
[EndpointSummary("管理员批量分配或解除选品操作员")]
|
||||
public async Task<ApiResult> AssignShopOperator(AssignRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.Assign(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>向当前账号导入未使用运单。</summary>
|
||||
[EndpointSummary("向当前账号导入未使用运单")]
|
||||
public async Task<ApiResult> AddWaybill(WaybillAddRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.AddWaybill(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>删除本人尚未预占的运单。</summary>
|
||||
[EndpointSummary("删除本人尚未预占的运单")]
|
||||
public async Task<ApiResult> DeleteWaybills(WaybillDeleteRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.DeleteWaybills(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>分页查询本人运单。</summary>
|
||||
[EndpointSummary("分页查询本人运单")]
|
||||
public Task<ShopPage<WaybillInfo>> GetWaybills(WaybillSearchRequest request, CancellationToken ct)
|
||||
=> shops.Waybills(request, false, ct);
|
||||
|
||||
/// <summary>管理员按订单跨账号查询运单。</summary>
|
||||
[EndpointSummary("管理员按订单跨账号查询运单")]
|
||||
public Task<ShopPage<WaybillInfo>> GetAllWaybills(WaybillSearchRequest request, CancellationToken ct)
|
||||
=> shops.Waybills(request, true, ct);
|
||||
|
||||
/// <summary>在同一事务中选择并确认使用运单。</summary>
|
||||
[EndpointSummary("在同一事务中选择并确认使用运单")]
|
||||
public async Task<ApiResult> SelectAndUseWaybill(WaybillSelectRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.SelectWaybill(request, true, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>为授权订单预占运单,优先匹配绑定订单。</summary>
|
||||
[EndpointSummary("为授权订单预占运单,优先匹配绑定订单")]
|
||||
public Task<WaybillInfo> ReserveWaybill(WaybillSelectRequest request, CancellationToken ct)
|
||||
=> shops.SelectWaybill(request, false, ct);
|
||||
|
||||
/// <summary>确认使用或释放本人预占运单。</summary>
|
||||
[EndpointSummary("确认使用或释放本人预占运单")]
|
||||
public async Task<ApiResult> UpdateWaybillUsage(WaybillUseRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.UseWaybill(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>保存授权订单物流优化结果。</summary>
|
||||
[EndpointSummary("保存授权订单物流优化结果")]
|
||||
public async Task<ApiResult> SaveExpressOptimizeResult(OptimizeResultRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.SaveOptimizeResult(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>查询授权店铺物流优化结果。</summary>
|
||||
[EndpointSummary("查询授权店铺物流优化结果")]
|
||||
public Task<ShopPage<OptimizeResultInfo>> GetExpressOptimizeResults(OrderSearchRequest request, CancellationToken ct)
|
||||
=> shops.OptimizeResults(request, ct);
|
||||
|
||||
/// <summary>保存或清空授权订单物流备注。</summary>
|
||||
[EndpointSummary("保存或清空授权订单物流备注")]
|
||||
public async Task<ApiResult> SaveExpressOptimizeRemark(OptimizeRemarkRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.SaveOptimizeRemark(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>查询授权店铺物流备注。</summary>
|
||||
[EndpointSummary("查询授权店铺物流备注")]
|
||||
public Task<ShopPage<OptimizeRemarkInfo>> GetExpressOptimizeRemarks(OrderSearchRequest request, CancellationToken ct)
|
||||
=> shops.OptimizeRemarks(request, ct);
|
||||
|
||||
/// <summary>维护本人的店铺默认售后地址。</summary>
|
||||
[EndpointSummary("维护本人的店铺默认售后地址")]
|
||||
public async Task<ApiResult> SaveShopDefaultAddress(AddressRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.SaveDefaultAddress(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>查询本人的店铺默认地址。</summary>
|
||||
[EndpointSummary("查询本人的店铺默认地址")]
|
||||
public Task<ShopPage<DefaultAddressInfo>> GetShopDefaultAddresses(AddressSearchRequest request, CancellationToken ct)
|
||||
=> shops.DefaultAddresses(request, ct);
|
||||
|
||||
/// <summary>批量删除本人的默认地址。</summary>
|
||||
[EndpointSummary("批量删除本人的默认地址")]
|
||||
public async Task<ApiResult> DeleteShopDefaultAddresses(ShopIdsRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.DeleteDefaultAddresses(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>保存授权店铺订单解密地址。</summary>
|
||||
[EndpointSummary("保存授权店铺订单解密地址")]
|
||||
public async Task<ApiResult> SaveOrderAddress(OrderAddressRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.SaveOrderAddress(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>查询授权店铺订单解密地址。</summary>
|
||||
[EndpointSummary("查询授权店铺订单解密地址")]
|
||||
public Task<ShopPage<OrderAddressInfo>> GetOrderAddresses(OrderSearchRequest request, CancellationToken ct)
|
||||
=> shops.OrderAddresses(request, ct);
|
||||
|
||||
/// <summary>维护当前主账号共享采购备注。</summary>
|
||||
[EndpointSummary("维护当前主账号共享采购备注")]
|
||||
public async Task<ApiResult> SaveBuyUrlNote(NoteRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.SaveBuyNote(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>查询当前主账号共享采购备注。</summary>
|
||||
[EndpointSummary("查询当前主账号共享采购备注")]
|
||||
public Task<ShopPage<BuyUrlNoteInfo>> GetBuyUrlNotes(NoteSearchRequest request, CancellationToken ct)
|
||||
=> shops.BuyNotes(request, ct);
|
||||
|
||||
/// <summary>导入或更新微信业务下的拼多多店铺资料。</summary>
|
||||
[EndpointSummary("导入或更新微信业务下的拼多多店铺资料")]
|
||||
public async Task<ApiResult> SavePddShop(PddSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.SavePdd(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>刷新微信侧拼多多店铺Cookie与状态。</summary>
|
||||
[EndpointSummary("刷新微信侧拼多多店铺Cookie与状态")]
|
||||
public async Task<ApiResult> UpdatePddShopLogin(CookieRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.UpdatePddCookies(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>删除本人微信侧拼多多店铺。</summary>
|
||||
[EndpointSummary("删除本人微信侧拼多多店铺")]
|
||||
public async Task<ApiResult> DeletePddShop(LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
await shops.DeletePdd(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>查询授权微信侧拼多多店铺。</summary>
|
||||
[EndpointSummary("查询授权微信侧拼多多店铺")]
|
||||
public Task<ShopPage<ShopInfo>> GetPddShops(ShopSearchRequest request, CancellationToken ct)
|
||||
=> shops.PddShops(request, ct);
|
||||
|
||||
/// <summary>管理员读取共享微信令牌列表缓存。</summary>
|
||||
[EndpointSummary("管理员读取共享微信令牌列表缓存")]
|
||||
public Task<TokensInfo> GetTokens(TokenRequest request, CancellationToken ct)
|
||||
=> shops.TokenList(request, ct);
|
||||
|
||||
/// <summary>获取授权微信店铺的e57令牌。</summary>
|
||||
[EndpointSummary("获取授权微信店铺的e57令牌")]
|
||||
public Task<JsonElement> GetShopToken(ShopIdRequest request, CancellationToken ct)
|
||||
=> shops.ShopToken(request, ct);
|
||||
|
||||
/// <summary>获取部署配置中的拼多多应用信息。</summary>
|
||||
[EndpointSummary("获取部署配置中的拼多多应用信息")]
|
||||
public Task<IReadOnlyList<PddClientInfo>> GetPddClients() => pddShops.GetPddClients();
|
||||
|
||||
/// <summary>查询本人或分配店铺的拼多多授权资料,读取bxx.shop_api_auth。</summary>
|
||||
[EndpointSummary("查询本人或分配店铺的拼多多授权资料,读取bxx.shop_api_auth")]
|
||||
public Task<ShopPage<ShopClientInfo>> GetPddShopClients(ShopClientSearchRequest request, CancellationToken ct)
|
||||
=> pddShops.GetShopClientInfo(request, ct);
|
||||
|
||||
/// <summary>检查bxx.shop归属后获取拼多多商城令牌。</summary>
|
||||
[EndpointSummary("检查bxx.shop归属后获取拼多多商城令牌")]
|
||||
public Task<JsonElement> GetPddShopToken(PddTokenRequest request, CancellationToken ct)
|
||||
=> pddShops.GetPddToken(request, ct);
|
||||
|
||||
/// <summary>管理员查询bxx.shop店铺并维护共享月度额度。</summary>
|
||||
[EndpointSummary("管理员查询bxx.shop店铺并维护共享月度额度")]
|
||||
public Task<ShopPage<ShopInfo>> GetAllPddShops(ShopSearchRequest request, CancellationToken ct)
|
||||
=> pddShops.GetAdminShops(request, ct);
|
||||
|
||||
/// <summary>新增微店店铺,重复导入时更新资料。</summary>
|
||||
[EndpointSummary("新增微店店铺,重复导入时更新资料")]
|
||||
public async Task<ApiResult> AddWeidianShop(WeidianSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
await pddShops.SaveWeidian(request, true, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>部分更新本人或已分配微店店铺。</summary>
|
||||
[EndpointSummary("部分更新本人或已分配微店店铺")]
|
||||
public async Task<ApiResult> UpdateWeidianShop(WeidianSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
await pddShops.SaveWeidian(request, false, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>更新授权微店店铺的Cookie与登录状态。</summary>
|
||||
[EndpointSummary("更新授权微店店铺的Cookie与登录状态")]
|
||||
public async Task<ApiResult> UpdateWeidianShopLogin(WeidianCookieRequest request, CancellationToken ct)
|
||||
{
|
||||
await pddShops.UpdateWeidianCookie(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>删除当前账号拥有的微店店铺。</summary>
|
||||
[EndpointSummary("删除当前账号拥有的微店店铺")]
|
||||
public async Task<ApiResult> DeleteWeidianShop(ShopIdRequest request, CancellationToken ct)
|
||||
{
|
||||
await pddShops.DeleteWeidian(request, ct);
|
||||
return ApiResult.Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
namespace Store.Shops.Dtos;
|
||||
|
||||
/// <summary>用户店铺默认售后地址的接口输出。</summary>
|
||||
public sealed class DefaultAddressInfo
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>软件账号。</summary>
|
||||
public string UserId { get; set; } = "";
|
||||
/// <summary>店铺ID。</summary>
|
||||
public string ShopId { get; set; } = "";
|
||||
/// <summary>省份。</summary>
|
||||
public string ProvinceName { get; set; } = "";
|
||||
/// <summary>城市。</summary>
|
||||
public string CityName { get; set; } = "";
|
||||
/// <summary>区县。</summary>
|
||||
public string CountyName { get; set; } = "";
|
||||
/// <summary>详细地址。</summary>
|
||||
public string DetailInfo { get; set; } = "";
|
||||
/// <summary>收件人电话。</summary>
|
||||
public string TelNumber { get; set; } = "";
|
||||
/// <summary>收件人姓名。</summary>
|
||||
public string Receiver { get; set; } = "";
|
||||
/// <summary>创建时间。</summary>
|
||||
public DateTime AddTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>订单解密地址的接口输出。</summary>
|
||||
public sealed class OrderAddressInfo
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>店铺ID。</summary>
|
||||
public string ShopId { get; set; } = "";
|
||||
/// <summary>订单ID。</summary>
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>解密后的地址内容。</summary>
|
||||
public string OrderAddress { get; set; } = "";
|
||||
/// <summary>创建时间。</summary>
|
||||
public DateTime AddTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>主账号共享采购备注的接口输出。</summary>
|
||||
public sealed class BuyUrlNoteInfo
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>所属主账号。</summary>
|
||||
public string UserName { get; set; } = "";
|
||||
/// <summary>商品编码。</summary>
|
||||
public string SpuCode { get; set; } = "";
|
||||
/// <summary>采购链接备注。</summary>
|
||||
public string BuyUrlNotes { get; set; } = "";
|
||||
/// <summary>创建时间。</summary>
|
||||
public DateTime AddTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>运单库存及分配状态的接口输出。</summary>
|
||||
public sealed class WaybillInfo
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>运单所属账号。</summary>
|
||||
public string UserName { get; set; } = "";
|
||||
/// <summary>物流公司编码。</summary>
|
||||
public string DeliveryId { get; set; } = "";
|
||||
/// <summary>物流公司名称。</summary>
|
||||
public string DeliveryName { get; set; } = "";
|
||||
/// <summary>运单号。</summary>
|
||||
public string WaybillId { get; set; } = "";
|
||||
/// <summary>预绑定订单ID。</summary>
|
||||
public string BindOrderId { get; set; } = "";
|
||||
/// <summary>分配订单ID。</summary>
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>使用店铺ID。</summary>
|
||||
public string ShopId { get; set; } = "";
|
||||
/// <summary>0未使用,1已预占,2已使用。</summary>
|
||||
public int Status { get; set; }
|
||||
/// <summary>入库时间。</summary>
|
||||
public DateTime AddTime { get; set; }
|
||||
/// <summary>正式使用时间。</summary>
|
||||
public DateTime? UseTime { get; set; }
|
||||
/// <summary>揽收时间。</summary>
|
||||
public DateTime? WaybillPickupTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>订单物流优化结果的接口输出。</summary>
|
||||
public sealed class OptimizeResultInfo
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>店铺ID。</summary>
|
||||
public string ShopId { get; set; } = "";
|
||||
/// <summary>订单ID。</summary>
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>补发处理结果。</summary>
|
||||
public string OptimizeRst { get; set; } = "";
|
||||
/// <summary>运单号。</summary>
|
||||
public string WaybillId { get; set; } = "";
|
||||
/// <summary>物流公司名称。</summary>
|
||||
public string DeliveryName { get; set; } = "";
|
||||
/// <summary>创建时间。</summary>
|
||||
public DateTime AddTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>订单物流优化备注的接口输出。</summary>
|
||||
public sealed class OptimizeRemarkInfo
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>店铺ID。</summary>
|
||||
public string ShopId { get; set; } = "";
|
||||
/// <summary>订单ID。</summary>
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>备注内容,允许清空。</summary>
|
||||
public string Remark { get; set; } = "";
|
||||
/// <summary>创建时间。</summary>
|
||||
public DateTime AddTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>ERP商户余额的接口输出。</summary>
|
||||
public sealed class ErpAccountInfo
|
||||
{
|
||||
/// <summary>账户ID。</summary>
|
||||
public long Id { get; set; }
|
||||
/// <summary>ERP用户ID。</summary>
|
||||
public long UserId { get; set; }
|
||||
/// <summary>账户名称。</summary>
|
||||
public string AccountName { get; set; } = "";
|
||||
/// <summary>账户状态。</summary>
|
||||
public int AccountStatus { get; set; }
|
||||
/// <summary>账户类型。</summary>
|
||||
public int AccountType { get; set; }
|
||||
/// <summary>可用余额。</summary>
|
||||
public decimal Balance { get; set; }
|
||||
/// <summary>冻结金额。</summary>
|
||||
public decimal FrozenAmount { get; set; }
|
||||
/// <summary>累计充值。</summary>
|
||||
public decimal RechargeAmountTotal { get; set; }
|
||||
/// <summary>已提现金额。</summary>
|
||||
public decimal WithdrawnAmount { get; set; }
|
||||
/// <summary>提现账户图片。</summary>
|
||||
public string WithdrawAccountImageUrl { get; set; } = "";
|
||||
/// <summary>创建人。</summary>
|
||||
public long? CreateBy { get; set; }
|
||||
/// <summary>修改人。</summary>
|
||||
public long? UpdateBy { get; set; }
|
||||
/// <summary>创建时间。</summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Dtos;
|
||||
|
||||
/// <summary>分页与统计选项。</summary>
|
||||
public class PageRequest
|
||||
{
|
||||
/// <summary>页码,从1开始。</summary>
|
||||
public int Page { get; set; } = 1;
|
||||
/// <summary>每页数量,上限500。</summary>
|
||||
public int PageSize { get; set; } = 50;
|
||||
/// <summary>是否跳过总数统计,跳过时总数为-1。</summary>
|
||||
public bool NotCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>微信店铺查询。</summary>
|
||||
public class ShopQueryRequest : ShopSearchRequest
|
||||
{
|
||||
/// <summary>是否补充ERP店铺和商户编号。</summary>
|
||||
public bool IsGetErpShopId { get; set; }
|
||||
/// <summary>兼容旧调用的标志,到期信息始终返回。</summary>
|
||||
public bool IsShowExpire { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>按登录名定位店铺。</summary>
|
||||
public class LoginRequest
|
||||
{
|
||||
/// <summary>店铺登录名。</summary>
|
||||
[Required(ErrorMessage = "店铺登录名不能为空")]
|
||||
public string LoginName { get; set; } = "";
|
||||
/// <summary>可选原店铺ID。</summary>
|
||||
public string? ShopId { get; set; }
|
||||
/// <summary>管理员操作的目标账号,普通调用忽略。</summary>
|
||||
public string? ShopUserName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>微信店铺新增或部分更新;null保留,空串清空。</summary>
|
||||
public class ShopSaveRequest : LoginRequest
|
||||
{
|
||||
/// <summary>登录密码。</summary>
|
||||
public string? LoginPass { get; set; }
|
||||
/// <summary>登录Cookie。</summary>
|
||||
public string? Cookie { get; set; }
|
||||
/// <summary>第二组Cookie。</summary>
|
||||
public string? Cookie2 { get; set; }
|
||||
/// <summary>风控参数。</summary>
|
||||
public string? AntiContents { get; set; }
|
||||
/// <summary>更新后的店铺ID。</summary>
|
||||
public string? NewShopId { get; set; }
|
||||
/// <summary>店铺名称。</summary>
|
||||
public string? ShopAccount { get; set; }
|
||||
/// <summary>逗号分隔的直属子账号。</summary>
|
||||
public string? SubUserName { get; set; }
|
||||
/// <summary>店铺分组。</summary>
|
||||
public string? GroupName { get; set; }
|
||||
/// <summary>备注。</summary>
|
||||
public string? Notes { get; set; }
|
||||
/// <summary>登录状态。</summary>
|
||||
public string? LoginStatus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>同登录名下授权店铺的登录状态更新。</summary>
|
||||
public class CookieRequest
|
||||
{
|
||||
/// <summary>店铺登录名。</summary>
|
||||
[Required(ErrorMessage = "店铺登录名不能为空")]
|
||||
public string LoginName { get; set; } = "";
|
||||
/// <summary>登录Cookie。</summary>
|
||||
public string? Cookie { get; set; }
|
||||
/// <summary>第二组Cookie。</summary>
|
||||
public string? Cookie2 { get; set; }
|
||||
/// <summary>登录状态。</summary>
|
||||
public string? LoginStatus { get; set; }
|
||||
/// <summary>第二组登录状态。</summary>
|
||||
public string? LoginStatus2 { get; set; }
|
||||
/// <summary>微信OpenId。</summary>
|
||||
public string? Openid { get; set; }
|
||||
/// <summary>拼多多用户ID。</summary>
|
||||
public string? PddId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>店铺ID批量请求。</summary>
|
||||
public class ShopIdsRequest
|
||||
{
|
||||
/// <summary>店铺ID数组。</summary>
|
||||
[Required(ErrorMessage = "店铺ID数组不能为空")]
|
||||
public string[] ShopIds { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>设置共享上传额度。</summary>
|
||||
public class QuotaRequest : ShopIdsRequest
|
||||
{
|
||||
/// <summary>剩余额度,不得小于0。</summary>
|
||||
public int RemainCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>批量分配选品操作员。</summary>
|
||||
public class AssignRequest
|
||||
{
|
||||
/// <summary>记录ID数组,优先于登录名。</summary>
|
||||
public int[]? DbIds { get; set; }
|
||||
/// <summary>店铺登录名数组。</summary>
|
||||
public string[]? ShopLoginNames { get; set; }
|
||||
/// <summary>操作员账号,空串表示解除分配。</summary>
|
||||
[Required(AllowEmptyStrings = true, ErrorMessage = "操作员账号不能为null,解除分配请传空串")]
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>查询授权店铺相关ERP商户。</summary>
|
||||
public class ErpRequest : PageRequest
|
||||
{
|
||||
/// <summary>可选店铺ID。</summary>
|
||||
public string? ShopId { get; set; }
|
||||
/// <summary>公司账号ID。</summary>
|
||||
public long? CorporateAccountUserId { get; set; }
|
||||
/// <summary>管理账号ID。</summary>
|
||||
public long? ManageUserId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>维护当前用户的店铺默认地址。</summary>
|
||||
public class AddressRequest : ShopIdRequest
|
||||
{
|
||||
/// <summary>省份。</summary>
|
||||
[Required(ErrorMessage = "省份不能为空")]
|
||||
public string ProvinceName { get; set; } = "";
|
||||
/// <summary>城市。</summary>
|
||||
[Required(ErrorMessage = "城市不能为空")]
|
||||
public string CityName { get; set; } = "";
|
||||
/// <summary>区县。</summary>
|
||||
[Required(ErrorMessage = "区县不能为空")]
|
||||
public string CountyName { get; set; } = "";
|
||||
/// <summary>详细地址。</summary>
|
||||
[Required(ErrorMessage = "详细地址不能为空")]
|
||||
public string DetailInfo { get; set; } = "";
|
||||
/// <summary>收件人电话。</summary>
|
||||
[Required(ErrorMessage = "收件人电话不能为空")]
|
||||
public string TelNumber { get; set; } = "";
|
||||
/// <summary>收件人姓名。</summary>
|
||||
[Required(ErrorMessage = "收件人姓名不能为空")]
|
||||
public string Receiver { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>查询本人默认地址。</summary>
|
||||
public class AddressSearchRequest : PageRequest
|
||||
{
|
||||
/// <summary>可选店铺ID数组。</summary>
|
||||
public string[]? ShopIds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>保存订单解密地址。</summary>
|
||||
public class OrderAddressRequest : ShopIdRequest
|
||||
{
|
||||
/// <summary>订单ID。</summary>
|
||||
[Required(ErrorMessage = "订单ID不能为空")]
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>解密地址内容。</summary>
|
||||
[Required(ErrorMessage = "解密地址内容不能为空")]
|
||||
public string OrderAddress { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>按订单查询授权店铺的数据。</summary>
|
||||
public class OrderSearchRequest : PageRequest
|
||||
{
|
||||
/// <summary>订单ID数组,地址查询必填。</summary>
|
||||
public string[]? OrderIds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>维护主账号共享采购备注。</summary>
|
||||
public class NoteRequest
|
||||
{
|
||||
/// <summary>商品编码。</summary>
|
||||
[Required(ErrorMessage = "商品编码不能为空")]
|
||||
public string SpuCode { get; set; } = "";
|
||||
/// <summary>采购备注。</summary>
|
||||
[Required(ErrorMessage = "采购备注不能为空")]
|
||||
public string BuyUrlNotes { get; set; } = "";
|
||||
/// <summary>可选目标主账号,必须匹配当前主账号。</summary>
|
||||
public string? MainUserName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>查询主账号采购备注。</summary>
|
||||
public class NoteSearchRequest : PageRequest
|
||||
{
|
||||
/// <summary>商品编码数组。</summary>
|
||||
[Required(ErrorMessage = "商品编码数组不能为空")]
|
||||
public string[] SpuCodes { get; set; } = [];
|
||||
/// <summary>可选目标主账号。</summary>
|
||||
public string? MainUserName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>微信侧拼多多店铺资料。</summary>
|
||||
public class PddSaveRequest : ShopSaveRequest
|
||||
{
|
||||
/// <summary>拼多多商城ID。</summary>
|
||||
public string? MallId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>查询运单库存。</summary>
|
||||
public class WaybillSearchRequest : OrderSearchRequest
|
||||
{
|
||||
/// <summary>状态数组:0未使用、1预占、2已使用。</summary>
|
||||
public int[]? Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>新增本人运单库存。</summary>
|
||||
public class WaybillAddRequest
|
||||
{
|
||||
/// <summary>物流公司编码。</summary>
|
||||
[Required(ErrorMessage = "物流公司编码不能为空")]
|
||||
public string DeliveryId { get; set; } = "";
|
||||
/// <summary>物流公司名称。</summary>
|
||||
[Required(ErrorMessage = "物流公司名称不能为空")]
|
||||
public string DeliveryName { get; set; } = "";
|
||||
/// <summary>运单号。</summary>
|
||||
[Required(ErrorMessage = "运单号不能为空")]
|
||||
public string WaybillId { get; set; } = "";
|
||||
/// <summary>预绑定订单ID。</summary>
|
||||
public string? BindOrderId { get; set; }
|
||||
/// <summary>揽收时间,与绑定订单至少填写一个。</summary>
|
||||
public DateTime? WaybillPickupTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>删除未使用运单。</summary>
|
||||
public class WaybillDeleteRequest
|
||||
{
|
||||
/// <summary>运单号数组。</summary>
|
||||
[Required(ErrorMessage = "运单号数组不能为空")]
|
||||
public string[] WaybillIds { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>为授权店铺订单分配运单。</summary>
|
||||
public class WaybillSelectRequest : ShopIdRequest
|
||||
{
|
||||
/// <summary>订单ID。</summary>
|
||||
[Required(ErrorMessage = "订单ID不能为空")]
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>订单时间,按次日揽收范围选择。</summary>
|
||||
public DateTime? OrderTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>确认使用或释放预占运单。</summary>
|
||||
public class WaybillUseRequest : WaybillSelectRequest
|
||||
{
|
||||
/// <summary>运单号。</summary>
|
||||
[Required(ErrorMessage = "运单号不能为空")]
|
||||
public string WaybillId { get; set; } = "";
|
||||
/// <summary>为true时释放,仅允许预占状态释放。</summary>
|
||||
public bool IsRollback { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>保存授权订单的物流优化结果。</summary>
|
||||
public class OptimizeResultRequest : ShopIdRequest
|
||||
{
|
||||
/// <summary>订单ID。</summary>
|
||||
[Required(ErrorMessage = "订单ID不能为空")]
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>补发结果。</summary>
|
||||
[Required(ErrorMessage = "补发结果不能为空")]
|
||||
public string OptimizeRst { get; set; } = "";
|
||||
/// <summary>运单号,null保留。</summary>
|
||||
public string? WaybillId { get; set; }
|
||||
/// <summary>物流公司名称,null保留。</summary>
|
||||
public string? DeliveryName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>保存订单物流备注。</summary>
|
||||
public class OptimizeRemarkRequest : ShopIdRequest
|
||||
{
|
||||
/// <summary>订单ID。</summary>
|
||||
[Required(ErrorMessage = "订单ID不能为空")]
|
||||
public string OrderId { get; set; } = "";
|
||||
/// <summary>备注内容,空串可清空,null不允许。</summary>
|
||||
[Required(AllowEmptyStrings = true, ErrorMessage = "备注不能为null,清空请传空串")]
|
||||
public string Remark { get; set; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>读取共享微信令牌缓存,仅管理员可调用。</summary>
|
||||
public class TokenRequest
|
||||
{
|
||||
/// <summary>缓存分组ID,默认1。</summary>
|
||||
public int DbId { get; set; } = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Store.Shops.Dtos;
|
||||
|
||||
/// <summary>微信店铺业务资料;列表不输出密码与Cookie。</summary>
|
||||
public sealed class ShopDetailsInfo
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>店铺登录名。</summary>
|
||||
public string LoginName { get; set; } = "";
|
||||
/// <summary>店铺ID。</summary>
|
||||
public string ShopId { get; set; } = "";
|
||||
/// <summary>店铺名称。</summary>
|
||||
public string ShopAccount { get; set; } = "";
|
||||
/// <summary>所属账号。</summary>
|
||||
public string UserName { get; set; } = "";
|
||||
/// <summary>分配子账号。</summary>
|
||||
public string SubUserName { get; set; } = "";
|
||||
/// <summary>店铺分组。</summary>
|
||||
public string GroupName { get; set; } = "";
|
||||
/// <summary>公司名称。</summary>
|
||||
public string CompanyName { get; set; } = "";
|
||||
/// <summary>备注。</summary>
|
||||
public string Notes { get; set; } = "";
|
||||
/// <summary>登录状态。</summary>
|
||||
public string LoginStatus { get; set; } = "";
|
||||
/// <summary>第二组登录状态。</summary>
|
||||
public string LoginStatus2 { get; set; } = "";
|
||||
/// <summary>选品操作员。</summary>
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
/// <summary>创建时间。</summary>
|
||||
public DateTime? AddTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
public DateTime? MdyTime { get; set; }
|
||||
/// <summary>操作员分配时间。</summary>
|
||||
public DateTime? AddSubSaTime { get; set; }
|
||||
/// <summary>微信OpenId。</summary>
|
||||
public string Openid { get; set; } = "";
|
||||
/// <summary>永久有效标志。</summary>
|
||||
public bool NeverExpires { get; set; }
|
||||
/// <summary>剩余上传额度。</summary>
|
||||
public int AddProductRemaining { get; set; }
|
||||
/// <summary>额度所属月份。</summary>
|
||||
public byte AddProductMonth { get; set; }
|
||||
/// <summary>到期Unix秒数。</summary>
|
||||
public int ExpireTime { get; set; }
|
||||
/// <summary>充值用户店铺是否已过期。</summary>
|
||||
public bool IsPointShopExpire { get; set; }
|
||||
/// <summary>ERP店铺ID。</summary>
|
||||
public long? ErpShopId { get; set; }
|
||||
/// <summary>ERP公司账号ID。</summary>
|
||||
public long? ErpCorporateAccountUserId { get; set; }
|
||||
/// <summary>ERP管理账号ID。</summary>
|
||||
public long? ErpManageUserId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>专用Cookie接口响应,仅返回已授权店铺的登录凭据。</summary>
|
||||
/// <param name="ShopId">微信店铺AppId。</param>
|
||||
/// <param name="Cookies">指定店铺的Cookie内容。</param>
|
||||
public sealed record ShopCookieInfo(string ShopId, string Cookies);
|
||||
|
||||
/// <summary>共享令牌列表和实际缓存刷新时间,仅管理员可读取。</summary>
|
||||
/// <param name="Tokens">上游返回的令牌列表JSON数组。</param>
|
||||
/// <param name="MdyTime">数据库缓存的最近刷新时间。</param>
|
||||
public sealed record TokensInfo(JsonElement Tokens, DateTime MdyTime);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Dtos;
|
||||
|
||||
public class ShopSearchRequest
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 50;
|
||||
public string? SearchId { get; set; }
|
||||
public string? SearchUserName { get; set; }
|
||||
public string? SearchLoginName { get; set; }
|
||||
public string? SearchNotes { get; set; }
|
||||
public string? SearchName { get; set; }
|
||||
public string? OperateSubSaName { get; set; }
|
||||
public string[]? ShopIds { get; set; }
|
||||
public string[]? ShopNames { get; set; }
|
||||
public string[]? ShopNotes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShopClientSearchRequest
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 500;
|
||||
public string[]? ShopIds { get; set; }
|
||||
}
|
||||
|
||||
public class ShopIdRequest
|
||||
{
|
||||
[Required(ErrorMessage = "店铺ID不能为空")]
|
||||
public string ShopId { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class PddTokenRequest
|
||||
{
|
||||
[Required(ErrorMessage = "拼多多商城ID不能为空")]
|
||||
public string MallId { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>更新时 null 保留原值,空串清空;归属账号只从当前身份获取。</summary>
|
||||
public sealed class WeidianSaveRequest
|
||||
{
|
||||
[Required(ErrorMessage = "店铺登录名不能为空")]
|
||||
public string LoginName { get; set; } = "";
|
||||
public string? ShopId { get; set; }
|
||||
public string? NewShopId { get; set; }
|
||||
public string? LoginPass { get; set; }
|
||||
public string? Cookie { get; set; }
|
||||
public string? Cookie2 { get; set; }
|
||||
public string? WdToken { get; set; }
|
||||
public string? MallId { get; set; }
|
||||
public string? ShopAccount { get; set; }
|
||||
public string? SubUserName { get; set; }
|
||||
public string? GroupName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string? LoginStatus { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WeidianCookieRequest : ShopIdRequest
|
||||
{
|
||||
public string? Cookie { get; set; }
|
||||
public string? WdToken { get; set; }
|
||||
public string? LoginStatus { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace Store.Shops.Dtos;
|
||||
|
||||
public sealed record ShopPage<T>(IReadOnlyList<T> Items, long Total);
|
||||
|
||||
/// <summary>列表只返回业务资料,登录密码、Cookie 和令牌不进入列表响应。</summary>
|
||||
public sealed class ShopInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string LoginName { get; set; } = "";
|
||||
public string ShopId { get; set; } = "";
|
||||
public string MallId { get; set; } = "";
|
||||
public string PddId { get; set; } = "";
|
||||
public string ShopAccount { get; set; } = "";
|
||||
public string UserName { get; set; } = "";
|
||||
public string SubUserName { get; set; } = "";
|
||||
public string CompanyName { get; set; } = "";
|
||||
public string Notes { get; set; } = "";
|
||||
public string LoginStatus { get; set; } = "";
|
||||
public string LoginStatus2 { get; set; } = "";
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
public int AddProductRemaining { get; set; }
|
||||
public int AddProductMonth { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>专用授权接口响应,仅允许读取本人或明确分配店铺的凭据。</summary>
|
||||
public sealed class ShopClientInfo
|
||||
{
|
||||
public string ShopId { get; set; } = "";
|
||||
public string AppKey { get; set; } = "";
|
||||
public string AppSecret { get; set; } = "";
|
||||
public string AccessToken { get; set; } = "";
|
||||
public string RefreshToken { get; set; } = "";
|
||||
public string ApiAuthStatus { get; set; } = "";
|
||||
public DateTime? ExpiresTime { get; set; }
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>供授权客户端使用的拼多多应用配置,通过部署配置注入。</summary>
|
||||
public sealed class PddClientInfo
|
||||
{
|
||||
public string Describe { get; set; } = "";
|
||||
public string ClientId { get; set; } = "";
|
||||
public string ClientSecret { get; set; } = "";
|
||||
public int MarketId { get; set; }
|
||||
public string RedirectUri { get; set; } = "";
|
||||
public bool IsCurrent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>主账号共享采购备注;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "bxx.product_buy_url_notes")]
|
||||
public sealed class BuyUrlNoteEntity
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>所属主账号。</summary>
|
||||
[Column(Name = "user_name")]
|
||||
public string UserName { get; set; } = "";
|
||||
|
||||
/// <summary>商品编码。</summary>
|
||||
[Column(Name = "spu_code")]
|
||||
public string SpuCode { get; set; } = "";
|
||||
|
||||
/// <summary>采购链接备注。</summary>
|
||||
[Column(Name = "buy_url_notes")]
|
||||
public string BuyUrlNotes { get; set; } = "";
|
||||
|
||||
/// <summary>创建时间。</summary>
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime AddTime { get; set; }
|
||||
|
||||
/// <summary>修改时间。</summary>
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>用户店铺默认售后地址;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.wx_shop_default_address")]
|
||||
public sealed class DefaultAddressEntity
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>软件账号。</summary>
|
||||
[Column(Name = "user_id")]
|
||||
public string UserId { get; set; } = "";
|
||||
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>省份。</summary>
|
||||
[Column(Name = "province_name")]
|
||||
public string ProvinceName { get; set; } = "";
|
||||
|
||||
/// <summary>城市。</summary>
|
||||
[Column(Name = "city_name")]
|
||||
public string CityName { get; set; } = "";
|
||||
|
||||
/// <summary>区县。</summary>
|
||||
[Column(Name = "county_name")]
|
||||
public string CountyName { get; set; } = "";
|
||||
|
||||
/// <summary>详细地址。</summary>
|
||||
[Column(Name = "detail_info")]
|
||||
public string DetailInfo { get; set; } = "";
|
||||
|
||||
/// <summary>收件人电话。</summary>
|
||||
[Column(Name = "tel_number")]
|
||||
public string TelNumber { get; set; } = "";
|
||||
|
||||
/// <summary>收件人姓名。</summary>
|
||||
[Column(Name = "user_name")]
|
||||
public string Receiver { get; set; } = "";
|
||||
|
||||
/// <summary>创建时间。</summary>
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime AddTime { get; set; }
|
||||
|
||||
/// <summary>修改时间。</summary>
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>ERP商户余额;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "erp.o_user_account")]
|
||||
public sealed class ErpAccountEntity
|
||||
{
|
||||
/// <summary>账户ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true)]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>ERP用户ID。</summary>
|
||||
[Column(Name = "user_id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>账户名称。</summary>
|
||||
[Column(Name = "account_name")]
|
||||
public string AccountName { get; set; } = "";
|
||||
|
||||
/// <summary>账户状态。</summary>
|
||||
[Column(Name = "account_status")]
|
||||
public int AccountStatus { get; set; }
|
||||
|
||||
/// <summary>账户类型。</summary>
|
||||
[Column(Name = "account_type")]
|
||||
public int AccountType { get; set; }
|
||||
|
||||
/// <summary>可用余额。</summary>
|
||||
[Column(Name = "balance")]
|
||||
public decimal Balance { get; set; }
|
||||
|
||||
/// <summary>冻结金额。</summary>
|
||||
[Column(Name = "frozen_amount")]
|
||||
public decimal FrozenAmount { get; set; }
|
||||
|
||||
/// <summary>累计充值。</summary>
|
||||
[Column(Name = "recharge_amount_total")]
|
||||
public decimal RechargeAmountTotal { get; set; }
|
||||
|
||||
/// <summary>已提现金额。</summary>
|
||||
[Column(Name = "withdrawn_amount")]
|
||||
public decimal WithdrawnAmount { get; set; }
|
||||
|
||||
/// <summary>提现账户图片。</summary>
|
||||
[Column(Name = "withdraw_account_image_url")]
|
||||
public string WithdrawAccountImageUrl { get; set; } = "";
|
||||
|
||||
/// <summary>创建人。</summary>
|
||||
[Column(Name = "create_by")]
|
||||
public long? CreateBy { get; set; }
|
||||
|
||||
/// <summary>修改人。</summary>
|
||||
[Column(Name = "update_by")]
|
||||
public long? UpdateBy { get; set; }
|
||||
|
||||
/// <summary>创建时间。</summary>
|
||||
[Column(Name = "create_time")]
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
/// <summary>修改时间。</summary>
|
||||
[Column(Name = "update_time")]
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>ERP公司与管理账号关联;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "erp.o_shop_group")]
|
||||
public sealed class ErpGroupEntity
|
||||
{
|
||||
/// <summary>公司账号ID。</summary>
|
||||
[Column(Name = "corporate_account_user_id")]
|
||||
public long CorporateAccountUserId { get; set; }
|
||||
|
||||
/// <summary>管理账号ID。</summary>
|
||||
[Column(Name = "manage_user_id")]
|
||||
public long ManageUserId { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>微信店铺与ERP账号关联;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "erp.o_shop")]
|
||||
public sealed class ErpShopEntity
|
||||
{
|
||||
/// <summary>ERP店铺ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true)]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>微信店铺AppId。</summary>
|
||||
[Column(Name = "app_key")]
|
||||
public string AppKey { get; set; } = "";
|
||||
|
||||
/// <summary>公司账号ID。</summary>
|
||||
[Column(Name = "corporate_account_user_id")]
|
||||
public long? CorporateAccountUserId { get; set; }
|
||||
|
||||
/// <summary>管理账号ID。</summary>
|
||||
[Column(Name = "manage_user_id")]
|
||||
public long? ManageUserId { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>订单物流优化备注;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.express_optimize_remark")]
|
||||
public sealed class OptimizeRemarkEntity
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>订单ID。</summary>
|
||||
[Column(Name = "order_id")]
|
||||
public string OrderId { get; set; } = "";
|
||||
|
||||
/// <summary>备注内容,允许清空。</summary>
|
||||
[Column(Name = "remark")]
|
||||
public string Remark { get; set; } = "";
|
||||
|
||||
/// <summary>创建时间。</summary>
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime AddTime { get; set; }
|
||||
|
||||
/// <summary>修改时间。</summary>
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>订单物流优化结果;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.express_optimize_rst")]
|
||||
public sealed class OptimizeResultEntity
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>订单ID。</summary>
|
||||
[Column(Name = "order_id")]
|
||||
public string OrderId { get; set; } = "";
|
||||
|
||||
/// <summary>补发处理结果。</summary>
|
||||
[Column(Name = "optimize_rst")]
|
||||
public string OptimizeRst { get; set; } = "";
|
||||
|
||||
/// <summary>运单号。</summary>
|
||||
[Column(Name = "waybill_id")]
|
||||
public string WaybillId { get; set; } = "";
|
||||
|
||||
/// <summary>物流公司名称。</summary>
|
||||
[Column(Name = "delivery_name")]
|
||||
public string DeliveryName { get; set; } = "";
|
||||
|
||||
/// <summary>创建时间。</summary>
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime AddTime { get; set; }
|
||||
|
||||
/// <summary>修改时间。</summary>
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>订单解密地址;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.order_decrypt_address")]
|
||||
public sealed class OrderAddressEntity
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>订单ID。</summary>
|
||||
[Column(Name = "order_id")]
|
||||
public string OrderId { get; set; } = "";
|
||||
|
||||
/// <summary>解密后的地址内容。</summary>
|
||||
[Column(Name = "order_address")]
|
||||
public string OrderAddress { get; set; } = "";
|
||||
|
||||
/// <summary>创建时间。</summary>
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime AddTime { get; set; }
|
||||
|
||||
/// <summary>修改时间。</summary>
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>微信业务使用的拼多多店铺;与 bxx.shop 分库存储。</summary>
|
||||
[Table(Name = "wxxd.pdd_shop")]
|
||||
public sealed class PddLoginShopEntity
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
/// <summary>店铺登录名。</summary>
|
||||
[Column(Name = "login_name")]
|
||||
public string LoginName { get; set; } = "";
|
||||
/// <summary>登录密码。</summary>
|
||||
[Column(Name = "login_pass")]
|
||||
public string LoginPass { get; set; } = "";
|
||||
/// <summary>登录Cookie。</summary>
|
||||
[Column(Name = "cookie")]
|
||||
public string Cookie { get; set; } = "";
|
||||
/// <summary>第二组Cookie。</summary>
|
||||
[Column(Name = "cookie2")]
|
||||
public string Cookie2 { get; set; } = "";
|
||||
/// <summary>风控参数。</summary>
|
||||
[Column(Name = "anti_contents")]
|
||||
public string AntiContents { get; set; } = "";
|
||||
/// <summary>拼多多用户ID。</summary>
|
||||
[Column(Name = "pddid")]
|
||||
public string PddId { get; set; } = "";
|
||||
/// <summary>商城ID。</summary>
|
||||
[Column(Name = "mall_id")]
|
||||
public string MallId { get; set; } = "";
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
/// <summary>店铺名称。</summary>
|
||||
[Column(Name = "shop_account")]
|
||||
public string ShopAccount { get; set; } = "";
|
||||
/// <summary>所属账号。</summary>
|
||||
[Column(Name = "user_name")]
|
||||
public string UserName { get; set; } = "";
|
||||
/// <summary>分配子账号。</summary>
|
||||
[Column(Name = "sub_user_name")]
|
||||
public string SubUserName { get; set; } = "";
|
||||
/// <summary>店铺分组。</summary>
|
||||
[Column(Name = "group_name")]
|
||||
public string GroupName { get; set; } = "";
|
||||
/// <summary>公司名称。</summary>
|
||||
[Column(Name = "company_name")]
|
||||
public string CompanyName { get; set; } = "";
|
||||
/// <summary>备注。</summary>
|
||||
[Column(Name = "notes")]
|
||||
public string Notes { get; set; } = "";
|
||||
/// <summary>登录状态。</summary>
|
||||
[Column(Name = "login_status")]
|
||||
public string LoginStatus { get; set; } = "";
|
||||
/// <summary>第二组登录状态。</summary>
|
||||
[Column(Name = "login_status2")]
|
||||
public string LoginStatus2 { get; set; } = "";
|
||||
/// <summary>选品操作员。</summary>
|
||||
[Column(Name = "operate_sub_sa_name")]
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
/// <summary>创建时间。</summary>
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime? AddTime { get; set; }
|
||||
/// <summary>修改时间。</summary>
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime? MdyTime { get; set; }
|
||||
/// <summary>操作员分配时间。</summary>
|
||||
[Column(Name = "add_sub_sa_time")]
|
||||
public DateTime? AddSubSaTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>旧库 bxx.shop 映射;不自动同步生产结构。</summary>
|
||||
[Table(Name = "bxx.shop")]
|
||||
public sealed class PddShopEntity
|
||||
{
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
[Column(Name = "login_name")]
|
||||
public string LoginName { get; set; } = "";
|
||||
[Column(Name = "login_pass")]
|
||||
public string LoginPass { get; set; } = "";
|
||||
[Column(Name = "cookie")]
|
||||
public string Cookie { get; set; } = "";
|
||||
[Column(Name = "cookie2")]
|
||||
public string Cookie2 { get; set; } = "";
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
[Column(Name = "mall_id")]
|
||||
public string MallId { get; set; } = "";
|
||||
[Column(Name = "shop_account")]
|
||||
public string ShopAccount { get; set; } = "";
|
||||
[Column(Name = "sub_user_name")]
|
||||
public string SubUserName { get; set; } = "";
|
||||
[Column(Name = "user_name")]
|
||||
public string UserName { get; set; } = "";
|
||||
[Column(Name = "group_name")]
|
||||
public string GroupName { get; set; } = "";
|
||||
[Column(Name = "company_name")]
|
||||
public string CompanyName { get; set; } = "";
|
||||
[Column(Name = "notes")]
|
||||
public string Notes { get; set; } = "";
|
||||
[Column(Name = "login_status")]
|
||||
public string LoginStatus { get; set; } = "";
|
||||
[Column(Name = "login_status2")]
|
||||
public string LoginStatus2 { get; set; } = "";
|
||||
[Column(Name = "operate_sub_sa_name")]
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime? AddTime { get; set; }
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime? MdyTime { get; set; }
|
||||
[Column(Name = "add_sub_sa_time")]
|
||||
public DateTime? AddSubSaTime { get; set; }
|
||||
[Column(Name = "pddid")]
|
||||
public string PddId { get; set; } = "";
|
||||
[Column(Name = "anti_contents")]
|
||||
public string AntiContents { get; set; } = "";
|
||||
[Column(Name = "add_product_remaining")]
|
||||
public int AddProductRemaining { get; set; }
|
||||
[Column(Name = "add_product_month")]
|
||||
public int AddProductMonth { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>选品店铺与操作员关联;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.product_select")]
|
||||
public sealed class SelectionEntity
|
||||
{
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>选品操作员账号。</summary>
|
||||
[Column(Name = "operate_sub_sa_name")]
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>旧库 bxx.shop_api_auth 映射;不自动同步生产结构。</summary>
|
||||
[Table(Name = "bxx.shop_api_auth")]
|
||||
public sealed class ShopApiAuthEntity
|
||||
{
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
[Column(Name = "ShopId")]
|
||||
public string ShopId { get; set; } = "";
|
||||
[Column(Name = "AppKey")]
|
||||
public string AppKey { get; set; } = "";
|
||||
[Column(Name = "AppSecret")]
|
||||
public string AppSecret { get; set; } = "";
|
||||
[Column(Name = "AccessToken")]
|
||||
public string AccessToken { get; set; } = "";
|
||||
[Column(Name = "RefreshToken")]
|
||||
public string RefreshToken { get; set; } = "";
|
||||
[Column(Name = "ApiAuthStatus")]
|
||||
public string ApiAuthStatus { get; set; } = "";
|
||||
[Column(Name = "ExpiresTime")]
|
||||
public DateTime? ExpiresTime { get; set; }
|
||||
[Column(Name = "UpdateTime")]
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace YourNamespace
|
||||
[Table(Name = "wxxd.shop")]
|
||||
public class ShopEntity
|
||||
{
|
||||
/// <summary>店铺是否永久有效;覆盖积分到期判断。</summary>
|
||||
[Column(Name = "never_expires")]
|
||||
public bool NeverExpires { get; set; }
|
||||
/// <summary>
|
||||
/// 表id
|
||||
/// </summary>
|
||||
@@ -141,4 +144,4 @@ namespace YourNamespace
|
||||
[Column(Name = "login_status2")]
|
||||
public string LoginStatus2 { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>旧库 bxx.shop_add_prod_count 映射;不自动同步生产结构。</summary>
|
||||
[Table(Name = "bxx.shop_add_prod_count")]
|
||||
public sealed class ShopProductQuotaEntity
|
||||
{
|
||||
[Column(Name = "shop_id", IsPrimary = true)]
|
||||
public string ShopId { get; set; } = "";
|
||||
[Column(Name = "shop_account")]
|
||||
public string ShopAccount { get; set; } = "";
|
||||
[Column(Name = "add_product_remaining")]
|
||||
public int AddProductRemaining { get; set; }
|
||||
[Column(Name = "add_product_month")]
|
||||
public int AddProductMonth { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>微信店铺每月共享上传额度;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.shop_add_prod_count")]
|
||||
public sealed class ShopQuotaEntity
|
||||
{
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id", IsPrimary = true)]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>店铺名称。</summary>
|
||||
[Column(Name = "shop_account")]
|
||||
public string ShopAccount { get; set; } = "";
|
||||
|
||||
/// <summary>剩余上传数量。</summary>
|
||||
[Column(Name = "add_product_remaining")]
|
||||
public int AddProductRemaining { get; set; }
|
||||
|
||||
/// <summary>额度所属月份。</summary>
|
||||
[Column(Name = "add_product_month")]
|
||||
public int AddProductMonth { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>选品店铺与操作员关联;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.third_product_select")]
|
||||
public sealed class ThirdSelectionEntity
|
||||
{
|
||||
/// <summary>店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>选品操作员账号。</summary>
|
||||
[Column(Name = "operate_sub_sa_name")]
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>e57微信令牌列表缓存;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.wx_token")]
|
||||
public sealed class TokenEntity
|
||||
{
|
||||
/// <summary>缓存分组ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>上游令牌列表JSON。</summary>
|
||||
[Column(Name = "tokens")]
|
||||
public string Tokens { get; set; } = "";
|
||||
|
||||
/// <summary>最近刷新时间。</summary>
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime MdyTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>运单库存及分配状态;映射旧库,不自动修改数据库结构。</summary>
|
||||
[Table(Name = "wxxd.express_optimize")]
|
||||
public sealed class WaybillEntity
|
||||
{
|
||||
/// <summary>记录ID。</summary>
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>运单所属账号。</summary>
|
||||
[Column(Name = "user_name")]
|
||||
public string UserName { get; set; } = "";
|
||||
|
||||
/// <summary>物流公司编码。</summary>
|
||||
[Column(Name = "delivery_id")]
|
||||
public string DeliveryId { get; set; } = "";
|
||||
|
||||
/// <summary>物流公司名称。</summary>
|
||||
[Column(Name = "delivery_name")]
|
||||
public string DeliveryName { get; set; } = "";
|
||||
|
||||
/// <summary>运单号。</summary>
|
||||
[Column(Name = "waybill_id")]
|
||||
public string WaybillId { get; set; } = "";
|
||||
|
||||
/// <summary>预绑定订单ID。</summary>
|
||||
[Column(Name = "bind_order_id")]
|
||||
public string BindOrderId { get; set; } = "";
|
||||
|
||||
/// <summary>分配订单ID。</summary>
|
||||
[Column(Name = "order_id")]
|
||||
public string OrderId { get; set; } = "";
|
||||
|
||||
/// <summary>使用店铺ID。</summary>
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
|
||||
/// <summary>0未使用,1已预占,2已使用。</summary>
|
||||
[Column(Name = "status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>入库时间。</summary>
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime AddTime { get; set; }
|
||||
|
||||
/// <summary>正式使用时间。</summary>
|
||||
[Column(Name = "use_time")]
|
||||
public DateTime? UseTime { get; set; }
|
||||
|
||||
/// <summary>揽收时间。</summary>
|
||||
[Column(Name = "waybill_pickup_time")]
|
||||
public DateTime? WaybillPickupTime { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
|
||||
namespace Store.Shops.Entities;
|
||||
|
||||
/// <summary>旧库 weidian.shop 映射;不自动同步生产结构。</summary>
|
||||
[Table(Name = "weidian.shop")]
|
||||
public sealed class WeidianShopEntity
|
||||
{
|
||||
[Column(Name = "id", IsPrimary = true, IsIdentity = true)]
|
||||
public int Id { get; set; }
|
||||
[Column(Name = "login_name")]
|
||||
public string LoginName { get; set; } = "";
|
||||
[Column(Name = "login_pass")]
|
||||
public string LoginPass { get; set; } = "";
|
||||
[Column(Name = "cookie")]
|
||||
public string Cookie { get; set; } = "";
|
||||
[Column(Name = "cookie2")]
|
||||
public string Cookie2 { get; set; } = "";
|
||||
[Column(Name = "shop_id")]
|
||||
public string ShopId { get; set; } = "";
|
||||
[Column(Name = "mall_id")]
|
||||
public string MallId { get; set; } = "";
|
||||
[Column(Name = "shop_account")]
|
||||
public string ShopAccount { get; set; } = "";
|
||||
[Column(Name = "sub_user_name")]
|
||||
public string SubUserName { get; set; } = "";
|
||||
[Column(Name = "user_name")]
|
||||
public string UserName { get; set; } = "";
|
||||
[Column(Name = "group_name")]
|
||||
public string GroupName { get; set; } = "";
|
||||
[Column(Name = "company_name")]
|
||||
public string CompanyName { get; set; } = "";
|
||||
[Column(Name = "notes")]
|
||||
public string Notes { get; set; } = "";
|
||||
[Column(Name = "login_status")]
|
||||
public string LoginStatus { get; set; } = "";
|
||||
[Column(Name = "login_status2")]
|
||||
public string LoginStatus2 { get; set; } = "";
|
||||
[Column(Name = "operate_sub_sa_name")]
|
||||
public string OperateSubSaName { get; set; } = "";
|
||||
[Column(Name = "add_time")]
|
||||
public DateTime? AddTime { get; set; }
|
||||
[Column(Name = "mdy_time")]
|
||||
public DateTime? MdyTime { get; set; }
|
||||
[Column(Name = "add_sub_sa_time")]
|
||||
public DateTime? AddSubSaTime { get; set; }
|
||||
[Column(Name = "wd_token")]
|
||||
public string WdToken { get; set; } = "";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using YourNamespace;
|
||||
|
||||
namespace Store.Shops.Mappers;
|
||||
|
||||
/// <summary>微信业务输出白名单;实体新增字段不会自动扩展接口响应。</summary>
|
||||
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.None)]
|
||||
internal partial class ShopDetailsMapper
|
||||
{
|
||||
/// <summary>映射微信店铺;到期和ERP字段由业务查询补充。</summary>
|
||||
public partial ShopDetailsInfo Map(ShopEntity value);
|
||||
|
||||
/// <summary>映射微信侧拼多多店铺,不输出登录凭据。</summary>
|
||||
public partial ShopInfo Map(PddLoginShopEntity value);
|
||||
|
||||
/// <summary>将用户店铺默认售后地址转换为接口数据。</summary>
|
||||
public partial DefaultAddressInfo Map(DefaultAddressEntity value);
|
||||
|
||||
/// <summary>将订单解密地址转换为接口数据。</summary>
|
||||
public partial OrderAddressInfo Map(OrderAddressEntity value);
|
||||
|
||||
/// <summary>将主账号共享采购备注转换为接口数据。</summary>
|
||||
public partial BuyUrlNoteInfo Map(BuyUrlNoteEntity value);
|
||||
|
||||
/// <summary>将运单库存及分配状态转换为接口数据。</summary>
|
||||
public partial WaybillInfo Map(WaybillEntity value);
|
||||
|
||||
/// <summary>将订单物流优化结果转换为接口数据。</summary>
|
||||
public partial OptimizeResultInfo Map(OptimizeResultEntity value);
|
||||
|
||||
/// <summary>将订单物流优化备注转换为接口数据。</summary>
|
||||
public partial OptimizeRemarkInfo Map(OptimizeRemarkEntity value);
|
||||
|
||||
/// <summary>将ERP商户余额转换为接口数据。</summary>
|
||||
public partial ErpAccountInfo Map(ErpAccountEntity value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Riok.Mapperly.Abstractions;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
|
||||
namespace Store.Shops.Mappers;
|
||||
|
||||
/// <summary>输出字段由 DTO 白名单限定,不映射列表凭据。</summary>
|
||||
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
|
||||
internal partial class ShopMapper
|
||||
{
|
||||
public partial ShopInfo ToInfo(PddShopEntity shop);
|
||||
public partial ShopClientInfo ToClientInfo(ShopApiAuthEntity auth);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
# Store.Shops 迁移说明
|
||||
|
||||
统一由 `Controllers/ShopController.cs` 暴露47个业务Action:微信39个、拼多多与微店8个。原 `bxx_shop` 独立控制器及重复的vdian别名已移除,原 `ShopController` 已重命名合并。
|
||||
旧逻辑来自 `KJD_WebService/BLL/BXX/BXXShopManage.cs`、`BLL/WXXD/WXShopManage.cs` 和 `BLL/WXXD/WXExpressOptimizeManage.cs`。
|
||||
微信店铺沿用现有实体、续费查询和被 Store.Users 引用的命名空间,补充旧表已有的 `never_expires` 字段。
|
||||
下文先介绍拼多多/微店接口,微信完整映射和部署要求见末节。
|
||||
|
||||
模块内部类型、文件及方法统一不带平台前缀,后续平台通过模块区分。核心服务为 `ShopService`;扩展查询、详情响应和映射分别为 `ShopQueryRequest`、`ShopDetailsInfo`、`ShopDetailsMapper`。`PddLoginShopEntity` 表示导入的拼多多登录记录,`PddShopEntity` 保留 bxx 店铺映射。
|
||||
原带平台标记的 Action 统一为 `SavePddShop`、`UpdatePddShopLogin`、`DeletePddShop`、`GetPddShops`、`GetTokens`、`GetShopToken`,客户端需同步更新路由。数据库映射、上游协议及已部署的 `Shops:E57WxAccounts` 配置键保持兼容。
|
||||
|
||||
## 调用方式
|
||||
|
||||
所有接口使用当前宿主认证,路由为 `POST /api/v1/shop/{Action}`,Action采用PascalCase业务命名。
|
||||
不再注册 `bxx_shop`、`wxxd_shop` 路由或旧下划线Action别名,客户端需按下表更新URL。
|
||||
合并仅调整HTTP入口:读取bxx库的授权资料、店铺列表和采购备注仍使用原业务服务、权限及表映射;微店业务一并保留。
|
||||
请求为 JSON DTO,字段使用 camelCase;不再发送旧版 `time/code/password/obj` 参数,也不再接受旧 GET 调用。
|
||||
这次重写不是旧 ASMX 的无改动替换,客户端需迁移认证、URL、JSON 参数和返回值解析。
|
||||
集合参数改为 JSON 数组,不再拼接逗号或 SQL 引号。
|
||||
|
||||
| 映射中的旧 action | 当前 action | 请求 |
|
||||
|---|---|---|
|
||||
| get_pdd_client_info | GetPddClients | 无业务参数 |
|
||||
| get_shop_client_info | GetPddShopClients | ShopClientSearchRequest |
|
||||
| e57_get_pdd_shop_token | GetPddShopToken | PddTokenRequest |
|
||||
| admin_get_shop_list | GetAllPddShops | ShopSearchRequest |
|
||||
| add_weidian_shop、add_vdian | AddWeidianShop | WeidianSaveRequest |
|
||||
| mdy_weidian_shop、mdy_vdian | UpdateWeidianShop | WeidianSaveRequest |
|
||||
| mdy_weidian_shop_cookie、mdy_vdian_cookie | UpdateWeidianShopLogin | WeidianCookieRequest |
|
||||
| del_weidian_shop | DeleteWeidianShop | ShopIdRequest |
|
||||
|
||||
`vdian` 的三个历史别名统一使用上述微店Action,不再单独暴露重复接口。
|
||||
`GetAllPddShops` 查询bxx.shop;`GetPddShops` 查询wxxd.pdd_shop,两套数据不混用。
|
||||
业务成功与失败由现有 ApiResult 管道包装,HTTP 200;无数据成功返回 `data: null`。
|
||||
列表位于 `data.items`,总数位于 `data.total`。
|
||||
e57 响应仅提取上游成功响应的 `data`,使用本系统的 code,不嵌套上游结果封套。
|
||||
认证、路由等 Action 外的错误继续使用宿主现有处理机制。
|
||||
|
||||
新增或重复导入微店示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"loginName": "店铺登录名",
|
||||
"shopId": "店铺ID",
|
||||
"shopAccount": "店铺名称",
|
||||
"notes": "备注"
|
||||
}
|
||||
```
|
||||
|
||||
更新时 `shopId` 定位原店铺,`newShopId` 修改店铺 ID;
|
||||
可选字段的 null/省略表示保留原值,空串表示清空。
|
||||
相同账号、登录名及店铺 ID 的重复新增转为更新。未指定店铺 ID 且登录名匹配多店时拒绝操作,防止批量误改。
|
||||
Cookie 更新须提供 Cookie 或登录状态;不允许仅凭 wdToken 更新。
|
||||
|
||||
## 权限与行为
|
||||
|
||||
- 管理列表要求当前账户的数据库角色为 SuperAdmin,保留原查询条件、稳定分页和每月 300 条额度重置。
|
||||
- 凭据查询仅可读取本人或明确分配给本人的店铺;管理员角色本身不扩大凭据范围。
|
||||
- 微店创建归属当前账号;本人或分配账号可更新,只有所有者能删除、修改分配账号。
|
||||
- `sub_user_name` 按逗号分隔的完整账号匹配,避免 owner 匹配 owner2。旧库若使用其他分隔格式,需先转换为该格式;账号之间不要带空格。
|
||||
- 管理列表不返回登录密码、Cookie、令牌。专用应用配置、店铺授权及令牌接口按其用途返回凭据,均禁止 HTTP 缓存。
|
||||
- 应用配置接口沿用已登录用户获取公共客户端配置的用途;不可把只允许服务端使用的其他应用密钥填入该配置。
|
||||
- 分配账号不能更改店铺分配关系。不存在或无权访问的资源统一报业务失败。
|
||||
- 空店铺 ID 的导入记录不创建共享额度;同一 shop_id 的登录记录共享额度。
|
||||
|
||||
## 部署
|
||||
|
||||
实体分别映射现有 `bxx.shop`、`bxx.shop_api_auth`、`bxx.shop_add_prod_count`、`weidian.shop`。
|
||||
不自动创建或修改生产表,不迁移数据。使用现有 IFreeSql 连接,部署账号需要访问上述库。
|
||||
额度和导入操作使用同一个 FreeSql 工作单元及 Serializable 隔离,失败整体回滚。
|
||||
生产表应为 InnoDB;`shop_add_prod_count.shop_id` 应有唯一约束(旧实现的 ON DUPLICATE KEY UPDATE 已依赖该约束)。
|
||||
建议确认微店 `(user_name, login_name, shop_id)` 与店铺 ID 查询索引,减少范围锁扫描。
|
||||
按数据库原字段保留月份额度语义;跨年同月识别需要另行升级字段,未在本次改变。
|
||||
|
||||
通过部署配置提供 `Shops:PddClients` 数组;不要把真实 ClientSecret 提交到源码。
|
||||
可使用环境变量:
|
||||
|
||||
```text
|
||||
Shops__PddClients__0__Describe
|
||||
Shops__PddClients__0__ClientId
|
||||
Shops__PddClients__0__ClientSecret
|
||||
Shops__PddClients__0__MarketId
|
||||
Shops__PddClients__0__RedirectUri
|
||||
Shops__PddClients__0__IsCurrent
|
||||
```
|
||||
|
||||
没有完整应用配置时接口返回明确业务错误。旧源码中的硬编码密钥未复制。
|
||||
e57 使用旧实现的固定 HTTPS 地址,正常验证证书、编码 mallId,失败最多重试一次,每次超时 30 秒;
|
||||
请求取消直接传递。未发送真实外部请求。
|
||||
模块服务沿用 ITransientDependency;Startup 注册共享 PddTokenClient。API 通过既有程序集发现加载控制器。
|
||||
|
||||
## 验证
|
||||
|
||||
```powershell
|
||||
dotnet test Tests/Store.Shops.Tests/Store.Shops.Tests.csproj
|
||||
dotnet test Tests/Store.Users.Tests/Store.Users.Tests.csproj
|
||||
dotnet build Store.API/Store.API.csproj
|
||||
dotnet run --project Store.API/Store.API.csproj
|
||||
```
|
||||
|
||||
店铺测试使用独立临时 SQLite 库和模拟 HTTP 上游,覆盖归属/分配权限、精确账号匹配、分页、重复导入、部分更新、Cookie、删除、额度重置与回滚、上游重试,以及真实 MVC 的路由、参数绑定、统一返回和匿名拒绝。
|
||||
SQLite 不验证 MySQL 范围锁、并发死锁或生产表实际结构;上线前仍需针对真实 MySQL 和 e57 联调。
|
||||
可继续使用代码审查检查迁移边界和客户端适配。
|
||||
|
||||
## 微信业务Action映射
|
||||
|
||||
微信39项业务已合入ShopController,使用 `POST /api/v1/shop/{Action}`;下表保留旧名字仅用于客户端迁移对照。
|
||||
旧 GET/ASMX 参数、签名和密码认证不再使用;客户端使用当前宿主令牌与 JSON DTO,字段 camelCase,批量编号传数组。
|
||||
无数据操作统一 ApiResult;分页结果为 `data.items/data.total`,支持 NotCount 的请求返回 total=-1 表示未统计。
|
||||
所有响应禁用缓存。通用列表和登录名查询不输出密码、Cookie;专用 Cookie/令牌接口检查资源范围后返回必要凭据。
|
||||
|
||||
| 旧Action | 当前Action | 请求DTO | 业务 |
|
||||
|---|---|---|---|
|
||||
| `get_shop_list` | `GetShops` | `ShopQueryRequest` | 查询本人或直属主账号分配的微信店铺 |
|
||||
| `get_shop_erp_merchant` | `GetErpMerchants` | `ErpRequest` | 查询授权店铺的ERP商户余额 |
|
||||
| `get_one_login_name_shop` | `GetShopsByLoginName` | `LoginRequest` | 查询登录名下的授权店铺 |
|
||||
| `get_one_shop_cookie_by_app_id` | `GetShopCookies` | `ShopIdsRequest` | 批量读取授权店铺Cookie |
|
||||
| `add_shop` | `AddShop` | `ShopSaveRequest` | 主账号新增微信店铺,重复记录拒绝创建 |
|
||||
| `del_shop` | `DeleteShop` | `LoginRequest` | 删除本人所属的店铺记录 |
|
||||
| `mdy_shop` | `UpdateShop` | `ShopSaveRequest` | 部分更新授权微信店铺 |
|
||||
| `mdy_shop_cookie_and_login_status` | `UpdateShopLogin` | `CookieRequest` | 按登录名刷新授权店铺Cookie与状态 |
|
||||
| `admin_get_shop_list` | `GetAllShops` | `ShopQueryRequest` | 管理员分页查询全部店铺 |
|
||||
| `sub_sa_get_select_prod_shop` | `GetSelectionShops` | `ShopQueryRequest` | 管理员按选品操作员查询店铺并刷新月度额度 |
|
||||
| `update_add_prod_remaining` | `UpdateShopQuota` | `QuotaRequest` | 管理员批量设置店铺上传剩余额度 |
|
||||
| `admin_mdy_shop` | `AdminUpdateShop` | `ShopSaveRequest` | 管理员修改指定账号的店铺 |
|
||||
| `admin_add_shop` | `AdminAddShop` | `ShopSaveRequest` | 管理员为指定账号新增店铺 |
|
||||
| `admin_del_shop` | `AdminDeleteShop` | `LoginRequest` | 管理员删除指定账号店铺 |
|
||||
| `set_shop_sub_sa` | `AssignShopOperator` | `AssignRequest` | 管理员批量分配或解除选品操作员 |
|
||||
| `add_waybill` | `AddWaybill` | `WaybillAddRequest` | 向当前账号导入未使用运单 |
|
||||
| `del_waybill` | `DeleteWaybills` | `WaybillDeleteRequest` | 删除本人尚未预占的运单 |
|
||||
| `get_waybill` | `GetWaybills` | `WaybillSearchRequest` | 分页查询本人运单 |
|
||||
| `get_waybill_no_user_name` | `GetAllWaybills` | `WaybillSearchRequest` | 管理员按订单跨账号查询运单 |
|
||||
| `select_and_set_waybill` | `SelectAndUseWaybill` | `WaybillSelectRequest` | 在同一事务中选择并确认使用运单 |
|
||||
| `select_waybill` | `ReserveWaybill` | `WaybillSelectRequest` | 为授权订单预占运单,优先匹配绑定订单 |
|
||||
| `update_used_waybill` | `UpdateWaybillUsage` | `WaybillUseRequest` | 确认使用或释放本人预占运单 |
|
||||
| `add_mdy_express_optimize_rst` | `SaveExpressOptimizeResult` | `OptimizeResultRequest` | 保存授权订单物流优化结果 |
|
||||
| `get_express_optimize_rst` | `GetExpressOptimizeResults` | `OrderSearchRequest` | 查询授权店铺物流优化结果 |
|
||||
| `add_mdy_express_optimize_remark` | `SaveExpressOptimizeRemark` | `OptimizeRemarkRequest` | 保存或清空授权订单物流备注 |
|
||||
| `get_express_optimize_remark` | `GetExpressOptimizeRemarks` | `OrderSearchRequest` | 查询授权店铺物流备注 |
|
||||
| `wx_add_mdy_shop_default_address` | `SaveShopDefaultAddress` | `AddressRequest` | 维护本人的店铺默认售后地址 |
|
||||
| `wx_get_shop_default_address_list` | `GetShopDefaultAddresses` | `AddressSearchRequest` | 查询本人的店铺默认地址 |
|
||||
| `wx_del_shop_default_address` | `DeleteShopDefaultAddresses` | `ShopIdsRequest` | 批量删除本人的默认地址 |
|
||||
| `wx_add_mdy_order_address` | `SaveOrderAddress` | `OrderAddressRequest` | 保存授权店铺订单解密地址 |
|
||||
| `wx_get_order_address` | `GetOrderAddresses` | `OrderSearchRequest` | 查询授权店铺订单解密地址 |
|
||||
| `wx_add_mdy_buy_url_notes` | `SaveBuyUrlNote` | `NoteRequest` | 维护当前主账号共享采购备注 |
|
||||
| `wx_get_buy_url_notes` | `GetBuyUrlNotes` | `NoteSearchRequest` | 查询当前主账号共享采购备注 |
|
||||
| `wx_add_mdy_29wx_pdd_shop` | `SavePddShop` | `PddSaveRequest` | 导入或更新微信业务下的拼多多店铺资料 |
|
||||
| `wx_mdy_29wx_pdd_cookie_and_login_status` | `UpdatePddShopLogin` | `CookieRequest` | 刷新微信侧拼多多店铺Cookie与状态 |
|
||||
| `wx_del_29wx_pdd_shop` | `DeletePddShop` | `LoginRequest` | 删除本人微信侧拼多多店铺 |
|
||||
| `wx_get_29wx_pdd_shop` | `GetPddShops` | `ShopSearchRequest` | 查询授权微信侧拼多多店铺 |
|
||||
| `e57_get_wx_tokens` | `GetTokens` | `TokenRequest` | 管理员读取共享微信令牌列表缓存 |
|
||||
| `e57_get_wx_shop_token` | `GetShopToken` | `ShopIdRequest` | 获取授权微信店铺的e57令牌 |
|
||||
|
||||
### 权限与兼容性说明
|
||||
|
||||
- 普通主账号查看自己的微信店铺;子账号必须同时满足父账号归属和完整账号分配匹配。账号列表用英文逗号分隔、不含空格。
|
||||
- AdminAddShop、AdminUpdateShop、AdminDeleteShop、GetAllShops、选品店铺查询、设置额度和操作员、跨账号运单查询、共享令牌列表要求数据库 SuperAdmin 角色。
|
||||
- 管理员的普通店铺、地址、单店令牌入口仍按本人的店铺范围处理;管理操作显式使用对应管理Action。
|
||||
- 管理员新增/修改/删除须传 shopUserName;普通新增仅允许主账号。微信重复新增报错;微信侧拼多多重复导入更新资料,并保留已有 Cookie。
|
||||
- 微信更新不传 shopId 时更新该登录名下授权记录;null 保留原值、空串清空。只有主账号或管理员可调整分配关系,分配对象须为直属子账号。
|
||||
- 同登录名 Cookie 更新始终限制账号范围,修复旧版跨账号同名更新问题。GetShopCookies 改传 shopIds 数组,包含无权访问的店铺时整次拒绝。
|
||||
- 默认地址仍按当前用户分别保存;订单解密地址和物流结果先限制可见店铺;采购备注由同一主账号及直属子账号共享。物流结果/备注保存必须传 shopId,不能凭订单号跨店覆盖。
|
||||
- ERP 商户只能通过授权店铺的 ERP 关联读取;公司账号展开其管理账号。结果按余额降序和记录 ID 稳定分页,total 使用真实数量,替代旧版特殊负数占位。
|
||||
- 普通店铺列表补充积分到期信息,never_expires 保留旧版有效期展示值 1893427200 且不判过期。isShowExpire 保留为兼容字段,旧实现同样未用于裁剪结果。
|
||||
- 选品模式查询两张选品表,保留每月300条共享额度重置;普通列表不触发额度重置。月份仍沿用旧字段,尚不区分跨年同月。
|
||||
|
||||
### 运单状态与事务
|
||||
|
||||
- status=0 未使用、1 预占、2 已正式使用;只允许删除本人 status=0 的记录。
|
||||
- 导入要求物流公司编码、名称、运单号,且揽收时间与绑定订单至少提供一个;运单号全库去重。
|
||||
- ReserveWaybill 优先选取绑定当前订单的运单,否则仅从订单日期次日揽收、未绑定的库存随机选取;缺少订单时间时不会随意占用普通库存。
|
||||
- SelectAndUseWaybill 在同一事务内分配并使用;普通库存不要求揽收时间匹配,但不会抢占绑定其他订单的运单。
|
||||
- 分配时立即保存 shop_id,重复请求复用本人同订单已预占/使用记录;同订单不能绑定其他店铺。确认使用是幂等操作,释放只允许 status=1,不允许释放已使用运单。
|
||||
- 事务、行锁和条件更新共同保护状态变更;取消或失败释放工作单元并回滚。保留旧业务的数据库处理范围,旧版 TODO 中未实现的 e57 发货提交不被伪装为已调用。
|
||||
|
||||
### 微信部署依赖与令牌配置
|
||||
|
||||
除现有 wxxd.shop、wxxd.user_points_shop_expire 外,读取/写入以下旧表,不自动创建或同步结构:
|
||||
|
||||
- wxxd.shop_add_prod_count、product_select、third_product_select、pdd_shop、wx_token。
|
||||
- wxxd.wx_shop_default_address、order_decrypt_address、express_optimize、express_optimize_rst、express_optimize_remark。
|
||||
- bxx.product_buy_url_notes;erp.o_shop、o_shop_group、o_user_account。
|
||||
|
||||
相关库需由现有 IFreeSql 连接访问。写表应使用 InnoDB;核对 shop_id、user_name/login_name/shop_id、waybill_id、user_name/order_id/status 及业务键查询索引,避免 Serializable 范围锁扩大扫描。
|
||||
保留旧表已有的唯一约束;本次不执行任何生产 DDL。旧的无 shop_id 物流结果记录需要补齐真实归属后才会出现在普通用户查询中。
|
||||
|
||||
共享微信令牌缓存每60分钟刷新一次,e57账号由部署配置提供,例如环境变量 `Shops__E57WxAccounts__1` 对应 dbId=1;多个分组分别配置各自 ID。
|
||||
不再硬编码旧测试账号。单店令牌复用 e57 HTTPS info?appId= 接口;共享列表使用 list?userName=,只接受 code=0 的结果。
|
||||
缓存写入使用实际 dbId,同时更新 mdy_time;有效缓存可直接读取,过期或损坏缓存刷新失败时返回业务错误。
|
||||
现有 PddTokenClient 扩展为共享 e57 客户端,保留拼多多入口签名和已有注册方式,不新增网络依赖。
|
||||
|
||||
### 本次验证边界
|
||||
|
||||
按用户要求,本次合并不新增、不运行测试;仅同步已有HTTP测试的路由、Action和服务注册,进行模块/API构建、Action命名及diff检查。
|
||||
以前执行的店铺测试不能作为此次路由变更后的测试结论。
|
||||
未连接真实 MySQL 或 e57;数据库结构、范围锁并发以及客户端调用仍需部署环境联调。可继续进行代码审查。
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Text.Json;
|
||||
using MAX.Framework.Commons;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
/// <summary>e57 店铺令牌查询;固定上游地址,失败重试一次,保留证书验证。</summary>
|
||||
public sealed class PddTokenClient(HttpClient http) : IDisposable
|
||||
{
|
||||
/// <summary>按拼多多商城ID获取授权资料。</summary>
|
||||
public Task<JsonElement> GetToken(string mallId, CancellationToken ct)
|
||||
=> GetResult("info/forPdd?mallId=" + Uri.EscapeDataString(mallId), "data", ct);
|
||||
|
||||
/// <summary>按微信AppId获取单店授权资料。</summary>
|
||||
public Task<JsonElement> GetShopToken(string appId, CancellationToken ct)
|
||||
=> GetResult("info?appId=" + Uri.EscapeDataString(appId), "data", ct);
|
||||
|
||||
/// <summary>按部署配置中的e57账号读取微信令牌列表。</summary>
|
||||
public Task<JsonElement> GetTokens(string userName, CancellationToken ct)
|
||||
=> GetResult("list?userName=" + Uri.EscapeDataString(userName), "list", ct);
|
||||
|
||||
/// <summary>解析上游业务状态,只返回指定数据节点,不泄露原始错误页面。</summary>
|
||||
private async Task<JsonElement> GetResult(string path, string property, CancellationToken ct)
|
||||
{
|
||||
var url = "https://po.e57.cn/prod-api/api/sys-api/system/open/api/shop/" + path;
|
||||
for (var attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await http.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode) continue;
|
||||
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct);
|
||||
var root = json.RootElement;
|
||||
if (root.ValueKind == JsonValueKind.Object
|
||||
&& root.TryGetProperty("code", out var code) && code.ValueKind == JsonValueKind.Number
|
||||
&& code.TryGetInt32(out var value) && value == 0
|
||||
&& root.TryGetProperty(property, out var data))
|
||||
return data.Clone();
|
||||
}
|
||||
catch (HttpRequestException) { /* 上游网络失败按旧接口约定重试一次。 */ }
|
||||
catch (JsonException) { /* 非 JSON 上游响应按业务失败处理,不返回原始页面。 */ }
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested) { /* HttpClient 超时。 */ }
|
||||
}
|
||||
throw new AppException("获取店铺令牌失败,请稍后重试");
|
||||
}
|
||||
|
||||
/// <summary>随容器关闭释放共享HTTP客户端。</summary>
|
||||
public void Dispose() => http.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopManagementService
|
||||
{
|
||||
public async Task<IReadOnlyList<PddClientInfo>> GetPddClients()
|
||||
{
|
||||
await Actor();
|
||||
var clients = configuration.GetSection("Shops:PddClients").Get<List<PddClientInfo>>() ?? [];
|
||||
if (clients.Count == 0 || clients.Any(c => string.IsNullOrWhiteSpace(c.ClientId)
|
||||
|| string.IsNullOrWhiteSpace(c.ClientSecret) || string.IsNullOrWhiteSpace(c.RedirectUri)))
|
||||
throw new AppException("尚未完整配置拼多多应用信息");
|
||||
return clients;
|
||||
}
|
||||
|
||||
public async Task<JsonElement> GetPddToken(PddTokenRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
if (!await db.Select<PddShopEntity>().Where(s => s.MallId == request.MallId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无读取令牌权限");
|
||||
return await e57.GetToken(request.MallId, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Security;
|
||||
using MAX.Framework.Validation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
using Store.Users.Contracts;
|
||||
using Store.Users.Contracts.Dtos;
|
||||
using Store.Users.Contracts.Enums;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
/// <summary>店铺业务入口;所有调用路径均从当前身份计算数据范围。</summary>
|
||||
public sealed partial class ShopManagementService(
|
||||
IFreeSql db, ICurrentUser currentUser, IUserQueryService users,
|
||||
IConfiguration configuration, PddTokenClient e57) : ITransientDependency
|
||||
{
|
||||
private async Task<UserInfo> Actor()
|
||||
{
|
||||
if (!currentUser.IsAuthenticated || string.IsNullOrWhiteSpace(currentUser.UserName))
|
||||
throw new UnauthorizedAccessException("请先登录");
|
||||
return await users.GetUser(currentUser.UserName);
|
||||
}
|
||||
|
||||
private static void ValidatePage(int page, int size)
|
||||
{
|
||||
if (page < 1 || size < 1 || size > 500 || (long)(page - 1) * size > int.MaxValue)
|
||||
throw new ValidationException("页码必须大于0,每页数量必须为1至500,分页偏移不能超限");
|
||||
}
|
||||
|
||||
public async Task<ShopPage<ShopClientInfo>> GetShopClientInfo(ShopClientSearchRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
ValidatePage(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
// 授权凭据即使由管理员请求,也必须属于本人或明确分配给本人的店铺。
|
||||
var query = db.Select<ShopApiAuthEntity>()
|
||||
.Where(a => db.Select<PddShopEntity>().Any(s => s.ShopId == a.ShopId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))))
|
||||
.WhereIf(request.ShopIds != null, a => request.ShopIds!.Contains(a.ShopId));
|
||||
var total = await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
var mapper = new ShopMapper();
|
||||
return new(rows.Select(mapper.ToClientInfo).ToList(), total);
|
||||
}
|
||||
|
||||
public async Task<ShopPage<ShopInfo>> GetAdminShops(ShopSearchRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
ValidatePage(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
if (actor.SaLevel != UserAdminLevel.SuperAdmin)
|
||||
throw new AppException("仅管理员可查询全部店铺");
|
||||
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var query = orm.Select<PddShopEntity>()
|
||||
.Where(s => s.Cookie == "" || s.ShopId != "")
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchId), s => s.ShopId.Contains(request.SearchId!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchUserName), s => s.UserName.Contains(request.SearchUserName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchLoginName), s => s.LoginName.Contains(request.SearchLoginName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchName), s => s.ShopAccount.Contains(request.SearchName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchNotes), s => s.Notes.Contains(request.SearchNotes!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.OperateSubSaName), s => s.OperateSubSaName == request.OperateSubSaName)
|
||||
.WhereIf(request.ShopIds != null, s => request.ShopIds!.Contains(s.ShopId))
|
||||
.WhereIf(request.ShopNames != null, s => request.ShopNames!.Contains(s.ShopAccount))
|
||||
.WhereIf(request.ShopNotes != null, s => request.ShopNotes!.Contains(s.Notes));
|
||||
var total = await query.CountAsync(ct);
|
||||
var shops = await query.OrderByDescending(s => s.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
var mapper = new ShopMapper();
|
||||
var result = shops.Select(mapper.ToInfo).ToList();
|
||||
// shortcut: 旧库只保存月份;需要跨年区分同月额度时升级为年月字段。
|
||||
var month = DateTime.Now.Month;
|
||||
// 同店铺可能有多条登录记录,按 shop_id 加锁并只重置一次共享额度。
|
||||
foreach (var group in result.Where(s => s.ShopId != "").GroupBy(s => s.ShopId).OrderBy(g => g.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var quota = await orm.Select<ShopProductQuotaEntity>().Where(q => q.ShopId == group.Key).ForUpdate().FirstAsync(ct);
|
||||
if (quota == null)
|
||||
{
|
||||
quota = new() { ShopId = group.Key, ShopAccount = group.First().ShopAccount, AddProductMonth = month, AddProductRemaining = 300 };
|
||||
await orm.Insert(quota).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else if (quota.AddProductMonth != month)
|
||||
{
|
||||
quota.AddProductMonth = month;
|
||||
quota.AddProductRemaining = 300;
|
||||
await orm.Update<ShopProductQuotaEntity>().Where(q => q.ShopId == group.Key)
|
||||
.Set(q => q.AddProductMonth, month).Set(q => q.AddProductRemaining, 300).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
await orm.Update<PddShopEntity>().Where(s => s.ShopId == group.Key)
|
||||
.Set(s => s.AddProductMonth, quota.AddProductMonth)
|
||||
.Set(s => s.AddProductRemaining, quota.AddProductRemaining).ExecuteAffrowsAsync(ct);
|
||||
foreach (var shop in group)
|
||||
{
|
||||
shop.AddProductMonth = quota.AddProductMonth;
|
||||
shop.AddProductRemaining = quota.AddProductRemaining;
|
||||
}
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
return new(result, total);
|
||||
}
|
||||
|
||||
public async Task SaveWeidian(WeidianSaveRequest request, bool create, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.LoginName.Length > 255)
|
||||
throw new ValidationException("店铺登录名不能超过255个字符");
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
using var work = db.CreateUnitOfWork();
|
||||
// 锁定匹配范围,避免同一账号并发导入产生重复记录;需要 InnoDB。
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var query = orm.Select<WeidianShopEntity>().Where(s => s.LoginName == request.LoginName)
|
||||
.Where(s => s.UserName == name || (!create && ("," + s.SubUserName + ",").Contains(member)))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId);
|
||||
var matches = await query.ForUpdate().ToListAsync(ct);
|
||||
if (matches.Count > 1)
|
||||
throw new AppException("登录名对应多个店铺,请指定店铺ID");
|
||||
var shop = matches.SingleOrDefault();
|
||||
if (shop == null)
|
||||
{
|
||||
if (!create) throw new AppException("店铺不存在或无操作权限");
|
||||
await orm.Insert(new WeidianShopEntity
|
||||
{
|
||||
LoginName = request.LoginName, UserName = name, ShopId = request.ShopId ?? "",
|
||||
LoginPass = request.LoginPass ?? "", Cookie = request.Cookie ?? "", Cookie2 = request.Cookie2 ?? "",
|
||||
WdToken = request.WdToken ?? "", MallId = request.MallId ?? "", ShopAccount = request.ShopAccount ?? "",
|
||||
SubUserName = request.SubUserName ?? "", GroupName = request.GroupName ?? "", Notes = request.Notes ?? "",
|
||||
LoginStatus = request.LoginStatus ?? "", AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (request.SubUserName != null && shop.UserName != name)
|
||||
throw new AppException("只有店铺所有者可以修改分配账号");
|
||||
var update = orm.Update<WeidianShopEntity>().Where(s => s.Id == shop.Id)
|
||||
.Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.LoginPass != null) update.Set(s => s.LoginPass, request.LoginPass);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.WdToken != null) update.Set(s => s.WdToken, request.WdToken);
|
||||
if (request.NewShopId != null) update.Set(s => s.ShopId, request.NewShopId);
|
||||
if (request.MallId != null) update.Set(s => s.MallId, request.MallId);
|
||||
if (request.ShopAccount != null) update.Set(s => s.ShopAccount, request.ShopAccount);
|
||||
if (request.SubUserName != null) update.Set(s => s.SubUserName, request.SubUserName);
|
||||
if (request.GroupName != null) update.Set(s => s.GroupName, request.GroupName);
|
||||
if (request.Notes != null) update.Set(s => s.Notes, request.Notes);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
public async Task UpdateWeidianCookie(WeidianCookieRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (string.IsNullOrEmpty(request.Cookie) && string.IsNullOrEmpty(request.LoginStatus))
|
||||
throw new ValidationException("Cookie和登录状态不能全为空");
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
var scope = db.Select<WeidianShopEntity>().Where(s => s.ShopId == request.ShopId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member)));
|
||||
if (!await scope.AnyAsync(ct)) throw new AppException("店铺不存在或无操作权限");
|
||||
var update = db.Update<WeidianShopEntity>().Where(s => s.ShopId == request.ShopId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member)))
|
||||
.Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.WdToken != null) update.Set(s => s.WdToken, request.WdToken);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteWeidian(ShopIdRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var affected = await db.Delete<WeidianShopEntity>()
|
||||
.Where(s => s.ShopId == request.ShopId && s.UserName == actor.UserName).ExecuteAffrowsAsync(ct);
|
||||
if (affected == 0) throw new AppException("店铺不存在或无删除权限");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>保存当前账号的店铺售后地址;同账号同店铺更新原记录。</summary>
|
||||
public async Task SaveDefaultAddress(AddressRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<DefaultAddressEntity>()
|
||||
.Where(a => a.UserId == actor.UserName && a.ShopId == request.ShopId).ForUpdate().FirstAsync(ct);
|
||||
if (row == null)
|
||||
await orm.Insert(new DefaultAddressEntity
|
||||
{
|
||||
UserId = actor.UserName, ShopId = request.ShopId, ProvinceName = request.ProvinceName,
|
||||
CityName = request.CityName, CountyName = request.CountyName, DetailInfo = request.DetailInfo,
|
||||
Receiver = request.Receiver, TelNumber = request.TelNumber, AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<DefaultAddressEntity>().Where(a => a.Id == row.Id)
|
||||
.Set(a => a.ProvinceName, request.ProvinceName).Set(a => a.CityName, request.CityName)
|
||||
.Set(a => a.CountyName, request.CountyName).Set(a => a.DetailInfo, request.DetailInfo)
|
||||
.Set(a => a.Receiver, request.Receiver).Set(a => a.TelNumber, request.TelNumber)
|
||||
.Set(a => a.MdyTime, DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>按当前账号和可见店铺范围查询默认地址。</summary>
|
||||
public async Task<ShopPage<DefaultAddressInfo>> DefaultAddresses(AddressSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<DefaultAddressEntity>().Where(a => a.UserId == actor.UserName && ids.Contains(a.ShopId))
|
||||
.WhereIf(request.ShopIds != null, a => request.ShopIds!.Contains(a.ShopId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>删除本人指定店铺的默认地址,不影响其他账号保存的地址。</summary>
|
||||
public async Task DeleteDefaultAddresses(ShopIdsRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.ShopIds);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
if (request.ShopIds.Except(ids).Any()) throw new AppException("包含无权访问的店铺");
|
||||
await db.Delete<DefaultAddressEntity>().Where(a => a.UserId == actor.UserName && request.ShopIds.Contains(a.ShopId)).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>以店铺及订单为键保存解密地址,禁止跨店覆盖。</summary>
|
||||
public async Task SaveOrderAddress(OrderAddressRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<OrderAddressEntity>().Where(a => a.ShopId == request.ShopId && a.OrderId == request.OrderId).ForUpdate().FirstAsync(ct);
|
||||
if (row == null)
|
||||
await orm.Insert(new OrderAddressEntity { ShopId = request.ShopId, OrderId = request.OrderId,
|
||||
OrderAddress = request.OrderAddress, AddTime = DateTime.Now, MdyTime = DateTime.Now }).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<OrderAddressEntity>().Where(a => a.Id == row.Id).Set(a => a.OrderAddress, request.OrderAddress)
|
||||
.Set(a => a.MdyTime, DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>查询订单解密地址,先限制当前用户可见店铺,再按订单过滤。</summary>
|
||||
public async Task<ShopPage<OrderAddressInfo>> OrderAddresses(OrderSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
if (request.OrderIds == null) throw new ValidationException("订单编号不能为空");
|
||||
Ids(request.OrderIds);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<OrderAddressEntity>().Where(a => ids.Contains(a.ShopId) && request.OrderIds.Contains(a.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>采购备注按可信主账号归属保存,直属子账号共享主账号的备注。</summary>
|
||||
public async Task SaveBuyNote(NoteRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var owner = MainName(await Actor());
|
||||
if (request.MainUserName != null && request.MainUserName != owner) throw new AppException("无权修改其他主账号备注");
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var row = await orm.Select<BuyUrlNoteEntity>().Where(n => n.UserName == owner && n.SpuCode == request.SpuCode).ForUpdate().FirstAsync(ct);
|
||||
if (row == null)
|
||||
await orm.Insert(new BuyUrlNoteEntity { UserName = owner, SpuCode = request.SpuCode,
|
||||
BuyUrlNotes = request.BuyUrlNotes, AddTime = DateTime.Now, MdyTime = DateTime.Now }).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<BuyUrlNoteEntity>().Where(n => n.Id == row.Id).Set(n => n.BuyUrlNotes, request.BuyUrlNotes)
|
||||
.Set(n => n.MdyTime, DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>分页查询当前主账号的采购备注。</summary>
|
||||
public async Task<ShopPage<BuyUrlNoteInfo>> BuyNotes(NoteSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
Ids(request.SpuCodes);
|
||||
var owner = MainName(await Actor());
|
||||
if (request.MainUserName != null && request.MainUserName != owner) throw new AppException("无权查询其他主账号备注");
|
||||
var query = db.Select<BuyUrlNoteEntity>().Where(n => n.UserName == owner && request.SpuCodes.Contains(n.SpuCode));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(n => n.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
using YourNamespace;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>选品列表跨月时重置300条额度,并同步店铺表;同店多条登录记录只处理一次。</summary>
|
||||
private async Task RefreshQuotas(List<ShopDetailsInfo> shops, CancellationToken ct)
|
||||
{
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
// shortcut: 旧表仅保存月份;需要区分跨年同月额度时升级为年月字段。
|
||||
var month = DateTime.Now.Month;
|
||||
foreach (var group in shops.Where(s => !string.IsNullOrEmpty(s.ShopId)).GroupBy(s => s.ShopId).OrderBy(g => g.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var quota = await orm.Select<ShopQuotaEntity>().Where(q => q.ShopId == group.Key).ForUpdate().FirstAsync(ct);
|
||||
if (quota == null)
|
||||
{
|
||||
quota = new() { ShopId = group.Key, ShopAccount = group.First().ShopAccount, AddProductMonth = month, AddProductRemaining = 300 };
|
||||
await orm.Insert(quota).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else if (quota.AddProductMonth != month)
|
||||
{
|
||||
quota.AddProductMonth = month;
|
||||
quota.AddProductRemaining = 300;
|
||||
await orm.Update<ShopQuotaEntity>().Where(q => q.ShopId == group.Key)
|
||||
.Set(q => q.AddProductMonth, month).Set(q => q.AddProductRemaining, 300).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
await orm.Update<ShopEntity>().Where(s => s.ShopId == group.Key)
|
||||
.Set(s => s.AddProductMonth, (byte)quota.AddProductMonth)
|
||||
.Set(s => s.AddProductRemaining, quota.AddProductRemaining).ExecuteAffrowsAsync(ct);
|
||||
foreach (var item in group)
|
||||
{
|
||||
item.AddProductMonth = (byte)quota.AddProductMonth;
|
||||
item.AddProductRemaining = quota.AddProductRemaining;
|
||||
}
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>管理员设置明确指定店铺的共享额度,同步列表读取的店铺字段。</summary>
|
||||
public async Task UpdateQuota(QuotaRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.ShopIds);
|
||||
if (request.RemainCount < 0) throw new ValidationException("剩余额度不能小于0");
|
||||
RequireAdmin(await Actor());
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var existing = await orm.Select<ShopQuotaEntity>().Where(q => request.ShopIds.Contains(q.ShopId)).ForUpdate().ToListAsync(ct);
|
||||
if (request.ShopIds.Except(existing.Select(q => q.ShopId)).Any()) throw new AppException("部分店铺尚未建立额度记录");
|
||||
await orm.Update<ShopQuotaEntity>().Where(q => request.ShopIds.Contains(q.ShopId))
|
||||
.Set(q => q.AddProductRemaining, request.RemainCount).ExecuteAffrowsAsync(ct);
|
||||
await orm.Update<ShopEntity>().Where(s => request.ShopIds.Contains(s.ShopId))
|
||||
.Set(s => s.AddProductRemaining, request.RemainCount).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>管理员按记录ID或登录名分配选品操作员,空账号表示解除分配。</summary>
|
||||
public async Task Assign(AssignRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.DbIds is not { Length: > 0 } && request.ShopLoginNames is not { Length: > 0 })
|
||||
throw new ValidationException("请选择店铺记录或登录名");
|
||||
if (request.DbIds?.Any(id => id <= 0) == true) throw new ValidationException("记录ID必须大于0");
|
||||
if (request.ShopLoginNames != null) Ids(request.ShopLoginNames);
|
||||
RequireAdmin(await Actor());
|
||||
if (request.OperateSubSaName != "") await users.GetUser(request.OperateSubSaName);
|
||||
var update = db.Update<ShopEntity>();
|
||||
if (request.DbIds is { Length: > 0 }) update.Where(s => request.DbIds.Contains(s.Id));
|
||||
else update.Where(s => request.ShopLoginNames!.Contains(s.LoginName));
|
||||
if (await update.Set(s => s.OperateSubSaName, request.OperateSubSaName)
|
||||
.Set(s => s.AddSubSaTime, DateTime.Now).ExecuteAffrowsAsync(ct) == 0)
|
||||
throw new AppException("未找到指定店铺");
|
||||
}
|
||||
|
||||
/// <summary>查询授权店铺对应的ERP商户余额,不能仅凭外部商户ID读取他人余额。</summary>
|
||||
public async Task<ShopPage<ErpAccountInfo>> ErpMerchants(ErpRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
if (request.CorporateAccountUserId == null && request.ManageUserId == null)
|
||||
throw new ValidationException("必须指定公司账号或管理账号");
|
||||
if (request.CorporateAccountUserId <= 0 || request.ManageUserId <= 0)
|
||||
throw new ValidationException("ERP账号ID必须大于0");
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var linked = await db.Select<ErpShopEntity>().Where(s => ids.Contains(s.AppKey))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.AppKey == request.ShopId).ToListAsync(ct);
|
||||
var managers = new List<long>();
|
||||
if (request.ManageUserId.HasValue)
|
||||
{
|
||||
if (!linked.Any(s => s.ManageUserId == request.ManageUserId)) throw new AppException("无权查询该ERP管理账号");
|
||||
managers.Add(request.ManageUserId.Value);
|
||||
}
|
||||
if (request.CorporateAccountUserId.HasValue)
|
||||
{
|
||||
if (!linked.Any(s => s.CorporateAccountUserId == request.CorporateAccountUserId))
|
||||
throw new AppException("无权查询该ERP公司账号");
|
||||
managers.AddRange(await db.Select<ErpGroupEntity>()
|
||||
.Where(g => g.CorporateAccountUserId == request.CorporateAccountUserId)
|
||||
.ToListAsync(g => g.ManageUserId, ct));
|
||||
}
|
||||
var query = db.Select<ErpAccountEntity>().Where(a => managers.Contains(a.UserId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Balance).OrderBy(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>保存授权店铺的物流优化结果;订单已关联其他店铺时拒绝覆盖。</summary>
|
||||
public async Task SaveOptimizeResult(OptimizeResultRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<OptimizeResultEntity>().Where(r => r.OrderId == request.OrderId).ForUpdate().FirstAsync(ct);
|
||||
if (row != null && row.ShopId != request.ShopId)
|
||||
throw new AppException("订单属于其他店铺,不能覆盖记录");
|
||||
if (row == null)
|
||||
await orm.Insert(new OptimizeResultEntity
|
||||
{
|
||||
ShopId = request.ShopId, OrderId = request.OrderId, OptimizeRst = request.OptimizeRst!,
|
||||
WaybillId = request.WaybillId ?? "", DeliveryName = request.DeliveryName ?? "",
|
||||
AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
{
|
||||
var update = orm.Update<OptimizeResultEntity>().Where(r => r.Id == row.Id)
|
||||
.Set(r => r.OptimizeRst, request.OptimizeRst!).Set(r => r.MdyTime, DateTime.Now);
|
||||
if (request.WaybillId != null) update.Set(r => r.WaybillId, request.WaybillId);
|
||||
if (request.DeliveryName != null) update.Set(r => r.DeliveryName, request.DeliveryName);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>分页查询可见店铺的物流优化结果,统计与数据使用相同范围。</summary>
|
||||
public async Task<ShopPage<OptimizeResultInfo>> OptimizeResults(OrderSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<OptimizeResultEntity>().Where(r => ids.Contains(r.ShopId))
|
||||
.WhereIf(request.OrderIds != null, r => request.OrderIds!.Contains(r.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(r => r.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>保存授权店铺的物流优化备注;订单已关联其他店铺时拒绝覆盖。</summary>
|
||||
public async Task SaveOptimizeRemark(OptimizeRemarkRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<OptimizeRemarkEntity>().Where(r => r.OrderId == request.OrderId).ForUpdate().FirstAsync(ct);
|
||||
if (row != null && row.ShopId != request.ShopId)
|
||||
throw new AppException("订单属于其他店铺,不能覆盖记录");
|
||||
if (row == null)
|
||||
await orm.Insert(new OptimizeRemarkEntity
|
||||
{
|
||||
ShopId = request.ShopId, OrderId = request.OrderId, Remark = request.Remark!,
|
||||
AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
{
|
||||
var update = orm.Update<OptimizeRemarkEntity>().Where(r => r.Id == row.Id)
|
||||
.Set(r => r.Remark, request.Remark!).Set(r => r.MdyTime, DateTime.Now);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>分页查询可见店铺的物流优化备注,统计与数据使用相同范围。</summary>
|
||||
public async Task<ShopPage<OptimizeRemarkInfo>> OptimizeRemarks(OrderSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<OptimizeRemarkEntity>().Where(r => ids.Contains(r.ShopId))
|
||||
.WhereIf(request.OrderIds != null, r => request.OrderIds!.Contains(r.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(r => r.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>查询微信业务下的拼多多店铺,保持与 bxx.shop 数据隔离。</summary>
|
||||
public async Task<ShopPage<ShopInfo>> PddShops(ShopSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
var query = db.Select<PddLoginShopEntity>().Where(s => s.UserName == name || ("," + s.SubUserName + ",").Contains(member))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchId), s => s.ShopId.Contains(request.SearchId!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchUserName), s => s.UserName.Contains(request.SearchUserName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchLoginName), s => s.LoginName.Contains(request.SearchLoginName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchName), s => s.ShopAccount.Contains(request.SearchName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchNotes), s => s.Notes.Contains(request.SearchNotes!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.OperateSubSaName), s => s.OperateSubSaName == request.OperateSubSaName)
|
||||
.WhereIf(request.ShopIds != null, s => request.ShopIds!.Contains(s.ShopId))
|
||||
.WhereIf(request.ShopNames != null, s => request.ShopNames!.Contains(s.ShopAccount))
|
||||
.WhereIf(request.ShopNotes != null, s => request.ShopNotes!.Contains(s.Notes));
|
||||
var count = await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(s => s.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>导入拼多多店铺,重复导入更新资料;已有Cookie只能由专用登录更新接口修改。</summary>
|
||||
public async Task SavePdd(PddSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
await ValidateAssignments(request.SubUserName, name);
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var rows = await orm.Select<PddLoginShopEntity>().Where(s => s.UserName == name && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId).ForUpdate().ToListAsync(ct);
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
await orm.Insert(new PddLoginShopEntity
|
||||
{
|
||||
UserName = name, LoginName = request.LoginName, LoginPass = request.LoginPass ?? "",
|
||||
Cookie = request.Cookie ?? "", Cookie2 = request.Cookie2 ?? "", AntiContents = request.AntiContents ?? "",
|
||||
ShopId = request.ShopId ?? "", MallId = request.MallId ?? "", ShopAccount = request.ShopAccount ?? "",
|
||||
SubUserName = request.SubUserName ?? "", GroupName = request.GroupName ?? "", Notes = request.Notes ?? "",
|
||||
LoginStatus = request.LoginStatus ?? "", AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ids = rows.Select(s => s.Id).ToArray();
|
||||
var update = orm.Update<PddLoginShopEntity>().Where(s => ids.Contains(s.Id)).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.LoginPass != null) update.Set(s => s.LoginPass, request.LoginPass);
|
||||
if (request.AntiContents != null) update.Set(s => s.AntiContents, request.AntiContents);
|
||||
if (request.NewShopId != null) update.Set(s => s.ShopId, request.NewShopId);
|
||||
if (request.MallId != null) update.Set(s => s.MallId, request.MallId);
|
||||
if (request.ShopAccount != null) update.Set(s => s.ShopAccount, request.ShopAccount);
|
||||
if (request.SubUserName != null) update.Set(s => s.SubUserName, request.SubUserName);
|
||||
if (request.GroupName != null) update.Set(s => s.GroupName, request.GroupName);
|
||||
if (request.Notes != null) update.Set(s => s.Notes, request.Notes);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>刷新本人或已分配拼多多店铺的Cookie、状态及拼多多用户ID。</summary>
|
||||
public async Task UpdatePddCookies(CookieRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.Cookie == null && request.Cookie2 == null && request.LoginStatus == null && request.LoginStatus2 == null && request.PddId == null)
|
||||
throw new ValidationException("至少提供一个登录状态字段");
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
var update = db.Update<PddLoginShopEntity>().Where(s => s.LoginName == request.LoginName
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
if (request.LoginStatus2 != null) update.Set(s => s.LoginStatus2, request.LoginStatus2);
|
||||
if (request.PddId != null) update.Set(s => s.PddId, request.PddId);
|
||||
if (await update.ExecuteAffrowsAsync(ct) == 0 && !await db.Select<PddLoginShopEntity>()
|
||||
.Where(s => s.LoginName == request.LoginName && (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无操作权限");
|
||||
}
|
||||
|
||||
/// <summary>删除当前账号自己创建的拼多多店铺,分配账号不能删除。</summary>
|
||||
public async Task DeletePdd(LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var count = await db.Delete<PddLoginShopEntity>().Where(s => s.UserName == actor.UserName && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId).ExecuteAffrowsAsync(ct);
|
||||
if (count == 0) throw new AppException("店铺不存在或无删除权限");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>获取当前用户授权店铺的微信令牌,外部请求不在数据库事务中执行。</summary>
|
||||
public async Task<JsonElement> ShopToken(ShopIdRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
await RequireShop(db, actor, request.ShopId, ct);
|
||||
return await tokens.GetShopToken(request.ShopId, ct);
|
||||
}
|
||||
|
||||
/// <summary>管理员读取共享令牌列表;缓存有效期60分钟,配置按缓存ID指定e57账号。</summary>
|
||||
public async Task<TokensInfo> TokenList(TokenRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.DbId <= 0) throw new ValidationException("缓存记录ID必须大于0");
|
||||
RequireAdmin(await Actor());
|
||||
var cached = await db.Select<TokenEntity>().Where(t => t.Id == request.DbId).FirstAsync(ct);
|
||||
if (cached != null && cached.MdyTime.AddMinutes(60) > DateTime.Now)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var json = JsonDocument.Parse(cached.Tokens);
|
||||
if (json.RootElement.ValueKind == JsonValueKind.Array) return new(json.RootElement.Clone(), cached.MdyTime);
|
||||
}
|
||||
catch (JsonException) { /* 旧缓存损坏时重新向上游获取,不返回无法解析的数据。 */ }
|
||||
}
|
||||
var account = configuration[$"Shops:E57WxAccounts:{request.DbId}"];
|
||||
if (string.IsNullOrWhiteSpace(account)) throw new AppException("尚未配置该缓存分组的e57微信账号");
|
||||
var list = await tokens.GetTokens(account, ct);
|
||||
if (list.ValueKind != JsonValueKind.Array) throw new AppException("e57返回的令牌列表格式无效");
|
||||
var now = DateTime.Now;
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var existing = await orm.Select<TokenEntity>().Where(t => t.Id == request.DbId).ForUpdate().FirstAsync(ct);
|
||||
if (existing == null)
|
||||
await orm.Insert(new TokenEntity { Id = request.DbId, Tokens = list.GetRawText(), MdyTime = now }).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<TokenEntity>().Where(t => t.Id == request.DbId)
|
||||
.Set(t => t.Tokens, list.GetRawText()).Set(t => t.MdyTime, now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
return new(list, now);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>查询本人运单;跨账号按订单查询仅供管理员使用且必须指定订单。</summary>
|
||||
public async Task<ShopPage<WaybillInfo>> Waybills(WaybillSearchRequest request, bool allUsers, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
if (request.Status?.Any(s => s is < 0 or > 2) == true) throw new ValidationException("运单状态只能为0、1、2");
|
||||
var actor = await Actor();
|
||||
if (allUsers)
|
||||
{
|
||||
RequireAdmin(actor);
|
||||
if (request.OrderIds == null) throw new ValidationException("订单编号不能为空");
|
||||
Ids(request.OrderIds);
|
||||
}
|
||||
var query = db.Select<WaybillEntity>().WhereIf(!allUsers, w => w.UserName == actor.UserName)
|
||||
.WhereIf(request.Status != null, w => request.Status!.Contains(w.Status))
|
||||
.WhereIf(request.OrderIds != null, w => request.OrderIds!.Contains(w.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(w => w.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>导入运单;揽收时间或绑定订单至少提供一个,运单号全库去重。</summary>
|
||||
public async Task AddWaybill(WaybillAddRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.WaybillPickupTime == null && string.IsNullOrWhiteSpace(request.BindOrderId))
|
||||
throw new ValidationException("揽收时间和绑定订单不能同时为空");
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
if (await orm.Select<WaybillEntity>().Where(w => w.WaybillId == request.WaybillId).ForUpdate().AnyAsync(ct))
|
||||
throw new AppException("运单号已存在");
|
||||
await orm.Insert(new WaybillEntity
|
||||
{
|
||||
UserName = actor.UserName, DeliveryId = request.DeliveryId, DeliveryName = request.DeliveryName,
|
||||
WaybillId = request.WaybillId, BindOrderId = request.BindOrderId ?? "", WaybillPickupTime = request.WaybillPickupTime,
|
||||
Status = 0, AddTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>只删除本人尚未使用的运单,已预占或使用的运单保持不变。</summary>
|
||||
public async Task DeleteWaybills(WaybillDeleteRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.WaybillIds);
|
||||
var actor = await Actor();
|
||||
await db.Delete<WaybillEntity>().Where(w => w.UserName == actor.UserName && w.Status == 0
|
||||
&& request.WaybillIds.Contains(w.WaybillId)).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>预占或直接使用运单;先复用同订单记录,再按绑定订单及次日揽收条件选择。</summary>
|
||||
public async Task<WaybillInfo> SelectWaybill(WaybillSelectRequest request, bool useImmediately, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.OrderTime?.Date >= DateTime.MaxValue.Date.AddDays(-2))
|
||||
throw new ValidationException("订单时间超出可匹配范围");
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<WaybillEntity>()
|
||||
.Where(w => w.UserName == actor.UserName && w.OrderId == request.OrderId && (w.Status == 1 || w.Status == 2))
|
||||
.OrderBy(w => w.Id).ForUpdate().FirstAsync(ct);
|
||||
if (row != null && !string.IsNullOrEmpty(row.ShopId) && row.ShopId != request.ShopId)
|
||||
throw new AppException("订单已绑定其他店铺的运单");
|
||||
if (row == null)
|
||||
{
|
||||
row = await orm.Select<WaybillEntity>().Where(w => w.UserName == actor.UserName && w.Status == 0
|
||||
&& w.BindOrderId == request.OrderId && (w.OrderId == "" || w.OrderId == null))
|
||||
.OrderBy("RAND()").ForUpdate().FirstAsync(ct);
|
||||
if (row == null && (request.OrderTime.HasValue || useImmediately))
|
||||
{
|
||||
var available = orm.Select<WaybillEntity>().Where(w => w.UserName == actor.UserName && w.Status == 0
|
||||
&& (w.OrderId == "" || w.OrderId == null) && (w.BindOrderId == "" || w.BindOrderId == null));
|
||||
if (!useImmediately)
|
||||
{
|
||||
var start = request.OrderTime!.Value.Date.AddDays(1);
|
||||
var end = start.AddDays(1);
|
||||
available.Where(w => w.WaybillPickupTime >= start && w.WaybillPickupTime < end);
|
||||
}
|
||||
row = await available.OrderBy("RAND()").ForUpdate().FirstAsync(ct);
|
||||
}
|
||||
if (row == null) throw new AppException("没有可用的运单号");
|
||||
}
|
||||
if (row.Status != 2)
|
||||
{
|
||||
var status = useImmediately ? 2 : 1;
|
||||
var useTime = useImmediately ? DateTime.Now : (DateTime?)null;
|
||||
// 行锁与状态条件一起保护分配;请求重试不会重复占用第二张运单。
|
||||
var affected = await orm.Update<WaybillEntity>().Where(w => w.Id == row.Id && w.Status == row.Status)
|
||||
.Set(w => w.OrderId, request.OrderId).Set(w => w.ShopId, request.ShopId)
|
||||
.Set(w => w.Status, status).Set(w => w.UseTime, useTime).ExecuteAffrowsAsync(ct);
|
||||
if (affected == 0 && row.Status == 0) throw new AppException("运单已被占用,请重试");
|
||||
row.OrderId = request.OrderId;
|
||||
row.ShopId = request.ShopId;
|
||||
row.Status = status;
|
||||
row.UseTime = useTime;
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
return new ShopDetailsMapper().Map(row);
|
||||
}
|
||||
|
||||
/// <summary>仅对本人的预占运单确认使用或释放;不能释放已正式使用的运单。</summary>
|
||||
public async Task UseWaybill(WaybillUseRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<WaybillEntity>().Where(w => w.UserName == actor.UserName
|
||||
&& w.OrderId == request.OrderId && w.WaybillId == request.WaybillId)
|
||||
.ForUpdate().FirstAsync(ct);
|
||||
if (row == null || (row.ShopId != "" && row.ShopId != null && row.ShopId != request.ShopId))
|
||||
throw new AppException("运单不存在或不属于指定店铺订单");
|
||||
if (!request.IsRollback && row.Status == 2) return;
|
||||
if (row.Status != 1) throw new AppException("只有预占运单可以确认或释放");
|
||||
var changed = await orm.Update<WaybillEntity>().Where(w => w.Id == row.Id && w.Status == 1)
|
||||
.Set(w => w.ShopId, request.IsRollback ? "" : request.ShopId)
|
||||
.Set(w => w.OrderId, request.IsRollback ? "" : request.OrderId)
|
||||
.Set(w => w.Status, request.IsRollback ? 0 : 2)
|
||||
.Set(w => w.UseTime, request.IsRollback ? null : (DateTime?)DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
if (changed != 1) throw new AppException("运单状态已变化,请重新查询");
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq.Expressions;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Security;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
using Store.Users.Contracts;
|
||||
using Store.Users.Contracts.Dtos;
|
||||
using Store.Users.Contracts.Enums;
|
||||
using Store.Users.Entities;
|
||||
using YourNamespace;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
/// <summary>微信店铺业务;账号身份来自服务端,普通子账号只能访问主账号明确分配的店铺。</summary>
|
||||
public sealed partial class ShopService(IFreeSql db, ICurrentUser currentUser, IUserQueryService users,
|
||||
PddTokenClient tokens, Microsoft.Extensions.Configuration.IConfiguration configuration) : ITransientDependency
|
||||
{
|
||||
/// <summary>读取当前数据库账户,避免相信客户端传来的身份或管理员标志。</summary>
|
||||
private async Task<UserInfo> Actor()
|
||||
{
|
||||
if (!currentUser.IsAuthenticated || string.IsNullOrWhiteSpace(currentUser.UserName))
|
||||
throw new UnauthorizedAccessException("请先登录");
|
||||
return await users.GetUser(currentUser.UserName);
|
||||
}
|
||||
|
||||
/// <summary>校验管理员专属入口。</summary>
|
||||
private static void RequireAdmin(UserInfo actor)
|
||||
{
|
||||
if (actor.SaLevel != UserAdminLevel.SuperAdmin) throw new AppException("仅管理员可执行此操作");
|
||||
}
|
||||
|
||||
/// <summary>主账号数据归属;子账号使用可信数据库记录中的父账号。</summary>
|
||||
private static string MainName(UserInfo actor)
|
||||
{
|
||||
if (actor.GroupId == UserGroup.MainAccount || actor.SaLevel == UserAdminLevel.SuperAdmin)
|
||||
return actor.UserName;
|
||||
if (string.IsNullOrWhiteSpace(actor.ParentUserName)) throw new AppException("子账号缺少所属主账号");
|
||||
return actor.ParentUserName;
|
||||
}
|
||||
|
||||
/// <summary>本人店铺或直属主账号分配的店铺;完整匹配逗号分隔账号,避免前缀越权。</summary>
|
||||
private static Expression<Func<ShopEntity, bool>> Scope(UserInfo actor)
|
||||
{
|
||||
var owner = MainName(actor);
|
||||
var main = owner == actor.UserName;
|
||||
var member = "," + actor.UserName + ",";
|
||||
return s => s.UserName == owner && (main || ("," + s.SubUserName + ",").Contains(member));
|
||||
}
|
||||
|
||||
/// <summary>为关联表查询提供当前用户可见店铺集合,不扩大管理员普通入口的范围。</summary>
|
||||
private Task<List<string>> VisibleIds(UserInfo actor, CancellationToken ct)
|
||||
=> db.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.ShopId != "").Distinct().ToListAsync(s => s.ShopId, ct);
|
||||
|
||||
/// <summary>关联数据操作必须先核对店铺归属;使用事务时传入同一工作单元。</summary>
|
||||
private static async Task RequireShop(IFreeSql orm, UserInfo actor, string shopId, CancellationToken ct)
|
||||
{
|
||||
if (!await orm.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.ShopId == shopId).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无操作权限");
|
||||
}
|
||||
|
||||
/// <summary>统一分页边界;DTO只声明基础必填规则。</summary>
|
||||
private static void Page(int page, int size)
|
||||
{
|
||||
if (page < 1 || size is < 1 or > 500 || (long)(page - 1) * size > int.MaxValue)
|
||||
throw new ValidationException("分页参数无效,每页数量必须为1至500");
|
||||
}
|
||||
|
||||
/// <summary>批量资源必须明确指定,禁止空数组退化为全表操作。</summary>
|
||||
private static void Ids(string[] ids)
|
||||
{
|
||||
if (ids.Length == 0 || ids.Any(string.IsNullOrWhiteSpace))
|
||||
throw new ValidationException("编号数组不能为空且不能包含空编号");
|
||||
}
|
||||
|
||||
/// <summary>分配账号必须是店铺主账号的直属子账号。</summary>
|
||||
private async Task ValidateAssignments(string? names, string owner)
|
||||
{
|
||||
if (names == null || names == "") return;
|
||||
foreach (var name in names.Split(','))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) || name != name.Trim())
|
||||
throw new ValidationException("分配账号须使用逗号分隔,不能包含空项或首尾空格");
|
||||
var child = await users.GetUser(name);
|
||||
if (child.ParentUserName != owner || child.SaLevel == UserAdminLevel.SuperAdmin)
|
||||
throw new AppException("只能分配所属主账号的直属子账号");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>普通或管理店铺列表;选品模式按两张选品表筛选并维护每月共享额度。</summary>
|
||||
public async Task<ShopPage<ShopDetailsInfo>> Search(ShopQueryRequest request, bool admin = false,
|
||||
bool selection = false, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
if (admin || selection) RequireAdmin(actor);
|
||||
var query = db.Select<ShopEntity>();
|
||||
if (!admin && !selection) query.Where(Scope(actor));
|
||||
else query.Where(s => s.Cookie == "" || s.ShopId != "");
|
||||
if (selection && !string.IsNullOrEmpty(request.OperateSubSaName))
|
||||
query.Where(s => db.Select<SelectionEntity>().Any(p => p.ShopId == s.ShopId && p.OperateSubSaName == request.OperateSubSaName)
|
||||
|| db.Select<ThirdSelectionEntity>().Any(p => p.ShopId == s.ShopId && p.OperateSubSaName == request.OperateSubSaName));
|
||||
else query.WhereIf(!string.IsNullOrEmpty(request.OperateSubSaName), s => s.OperateSubSaName == request.OperateSubSaName);
|
||||
query.WhereIf(!string.IsNullOrEmpty(request.SearchId), s => s.ShopId.Contains(request.SearchId!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchUserName), s => s.UserName.Contains(request.SearchUserName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchLoginName), s => s.LoginName.Contains(request.SearchLoginName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchName), s => s.ShopAccount.Contains(request.SearchName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchNotes), s => s.Notes.Contains(request.SearchNotes!))
|
||||
.WhereIf(request.ShopIds != null, s => request.ShopIds!.Contains(s.ShopId))
|
||||
.WhereIf(request.ShopNames != null, s => request.ShopNames!.Contains(s.ShopAccount))
|
||||
.WhereIf(request.ShopNotes != null, s => request.ShopNotes!.Contains(s.Notes));
|
||||
var total = await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(s => s.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
var mapper = new ShopDetailsMapper();
|
||||
var result = rows.Select(mapper.Map).ToList();
|
||||
var ids = rows.Select(s => s.ShopId).Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
|
||||
var expires = await db.Select<ShopExpireEntity>().Where(e => ids.Contains(e.ShopId)).ToListAsync(ct);
|
||||
var paid = actor.Authority?.Contains("充值用户") == true;
|
||||
if (!admin && !string.IsNullOrEmpty(actor.ParentUserName))
|
||||
paid |= (await users.GetUser(actor.ParentUserName)).Authority?.Contains("充值用户") == true;
|
||||
var now = DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
foreach (var item in result)
|
||||
{
|
||||
item.ExpireTime = item.NeverExpires ? 1893427200 : expires.FirstOrDefault(e => e.ShopId == item.ShopId)?.ExpireTime ?? 0;
|
||||
item.IsPointShopExpire = !admin && paid && !item.NeverExpires && item.ExpireTime < now;
|
||||
}
|
||||
if (request.IsGetErpShopId)
|
||||
{
|
||||
var erp = await db.Select<ErpShopEntity>().Where(e => ids.Contains(e.AppKey)).OrderBy(e => e.Id).ToListAsync(ct);
|
||||
foreach (var item in result)
|
||||
{
|
||||
var linked = erp.FirstOrDefault(e => e.AppKey == item.ShopId);
|
||||
item.ErpShopId = linked?.Id;
|
||||
item.ErpCorporateAccountUserId = linked?.CorporateAccountUserId;
|
||||
item.ErpManageUserId = linked?.ManageUserId;
|
||||
}
|
||||
}
|
||||
if (selection) await RefreshQuotas(result, ct);
|
||||
return new(result, total);
|
||||
}
|
||||
|
||||
/// <summary>查询同一登录名的授权店铺,供客户端判断新增或更新。</summary>
|
||||
public async Task<IReadOnlyList<ShopDetailsInfo>> ByLogin(LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var rows = await db.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName)
|
||||
.OrderBy(s => s.Id).ToListAsync(ct);
|
||||
return rows.Select(new ShopDetailsMapper().Map).ToList();
|
||||
}
|
||||
|
||||
/// <summary>批量读取授权店铺Cookie;对重复店铺ID按最新记录确定结果。</summary>
|
||||
public async Task<IReadOnlyList<ShopCookieInfo>> Cookies(ShopIdsRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.ShopIds);
|
||||
var actor = await Actor();
|
||||
var rows = await db.Select<ShopEntity>().Where(Scope(actor)).Where(s => request.ShopIds.Contains(s.ShopId))
|
||||
.OrderByDescending(s => s.Id).ToListAsync(ct);
|
||||
if (request.ShopIds.Except(rows.Select(s => s.ShopId)).Any()) throw new AppException("包含无权访问的店铺");
|
||||
return rows.DistinctBy(s => s.ShopId).Select(s => new ShopCookieInfo(s.ShopId, s.Cookie)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>新增或修改微信店铺;新增重复时报错,不使用微店的覆盖导入行为。</summary>
|
||||
public async Task Save(ShopSaveRequest request, bool create, bool admin, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
if (admin) RequireAdmin(actor);
|
||||
if (admin && string.IsNullOrWhiteSpace(request.ShopUserName)) throw new ValidationException("管理员操作必须指定店铺所属账号");
|
||||
var owner = admin ? (await users.GetUser(request.ShopUserName!)).UserName : MainName(actor);
|
||||
if (create && !admin && owner != actor.UserName) throw new AppException("只有主账号可新增店铺");
|
||||
if (request.SubUserName != null && !admin && owner != actor.UserName) throw new AppException("子账号不能修改店铺分配关系");
|
||||
await ValidateAssignments(request.SubUserName, owner);
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var query = orm.Select<ShopEntity>().Where(s => s.UserName == owner && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId);
|
||||
if (!admin) query.Where(Scope(actor));
|
||||
var rows = await query.ForUpdate().ToListAsync(ct);
|
||||
if (create)
|
||||
{
|
||||
if (rows.Count != 0) throw new AppException("店铺已存在");
|
||||
await orm.Insert(new ShopEntity
|
||||
{
|
||||
UserName = owner, LoginName = request.LoginName, LoginPass = request.LoginPass ?? "",
|
||||
ShopId = request.ShopId ?? "", ShopAccount = request.ShopAccount ?? "", Cookie = request.Cookie ?? "",
|
||||
Cookie2 = request.Cookie2 ?? "", AntiContents = request.AntiContents ?? "", SubUserName = request.SubUserName ?? "",
|
||||
GroupName = request.GroupName ?? "", Notes = request.Notes ?? "", Openid = "", CompanyName = "",
|
||||
LoginStatus = request.LoginStatus ?? "", LoginStatus2 = "", OperateSubSaName = "",
|
||||
AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rows.Count == 0) throw new AppException("店铺不存在或无操作权限");
|
||||
var ids = rows.Select(s => s.Id).ToArray();
|
||||
var update = orm.Update<ShopEntity>().Where(s => ids.Contains(s.Id)).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.LoginPass != null) update.Set(s => s.LoginPass, request.LoginPass);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.AntiContents != null) update.Set(s => s.AntiContents, request.AntiContents);
|
||||
if (request.NewShopId != null) update.Set(s => s.ShopId, request.NewShopId);
|
||||
if (request.ShopAccount != null) update.Set(s => s.ShopAccount, request.ShopAccount);
|
||||
if (request.SubUserName != null) update.Set(s => s.SubUserName, request.SubUserName);
|
||||
if (request.GroupName != null) update.Set(s => s.GroupName, request.GroupName);
|
||||
if (request.Notes != null) update.Set(s => s.Notes, request.Notes);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>更新登录名下全部授权店铺的Cookie与状态,禁止跨账号同名更新。</summary>
|
||||
public async Task UpdateCookies(CookieRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.Cookie == null && request.Cookie2 == null && request.LoginStatus == null && request.LoginStatus2 == null && request.Openid == null)
|
||||
throw new ValidationException("至少提供一个登录状态字段");
|
||||
var actor = await Actor();
|
||||
var update = db.Update<ShopEntity>().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
if (request.LoginStatus2 != null) update.Set(s => s.LoginStatus2, request.LoginStatus2);
|
||||
if (request.Openid != null) update.Set(s => s.Openid, request.Openid);
|
||||
if (await update.ExecuteAffrowsAsync(ct) == 0
|
||||
&& !await db.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无操作权限");
|
||||
}
|
||||
|
||||
/// <summary>删除所属账号的店铺记录;不删除独立的续费记录。</summary>
|
||||
public async Task Delete(LoginRequest request, bool admin, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
if (admin) RequireAdmin(actor);
|
||||
if (admin && string.IsNullOrWhiteSpace(request.ShopUserName)) throw new ValidationException("必须指定店铺所属账号");
|
||||
var owner = admin ? request.ShopUserName! : actor.UserName;
|
||||
var deleted = await db.Delete<ShopEntity>().Where(s => s.UserName == owner && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId).ExecuteAffrowsAsync(ct);
|
||||
if (deleted == 0) throw new AppException("店铺不存在或无删除权限");
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,10 @@ namespace Store.Shops
|
||||
|
||||
public void Register(IServiceRegistrar registrar)
|
||||
{
|
||||
|
||||
registrar.RegisterSingleton(new Services.PddTokenClient(new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
}));
|
||||
}
|
||||
|
||||
public void Start(IServiceProvider serviceProvider)
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Store.Framework\Store.Framework.csproj" />
|
||||
<ProjectReference Include="..\Store.Shops.Contracts\Store.Shops.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Store.Users.Contracts\Store.Users.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in new issue
Block a user