提现修改.

This commit is contained in:
18631081161 2026-04-17 19:10:41 +08:00
parent dd2f41b541
commit 45e29fee4b
14 changed files with 2303 additions and 69 deletions

View File

@ -5,7 +5,9 @@
<el-radio-group v-model="statusFilter" @change="loadData">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="Pending">待处理</el-radio-button>
<el-radio-button label="WaitConfirm">待确认</el-radio-button>
<el-radio-button label="Completed">已完成</el-radio-button>
<el-radio-button label="Rejected">已拒绝</el-radio-button>
</el-radio-group>
</div>
@ -42,6 +44,9 @@
<el-button type="success" size="small" plain @click="handleAction(row, 'approve')">通过</el-button>
<el-button type="danger" size="small" plain @click="handleAction(row, 'reject')">拒绝</el-button>
</template>
<template v-else-if="row.status === 'WaitConfirm'">
<span style="color: #e6a23c">等待用户确认收款</span>
</template>
<template v-else>
<span style="color: #999">已处理</span>
</template>
@ -76,21 +81,32 @@ async function loadData() {
async function handleAction(row, action) {
const labels = { approve: '通过', reject: '拒绝', processing: '标记为处理中' }
try {
await ElMessageBox.confirm(`确定${labels[action]}该提现申请?`, '提示', { type: 'warning' })
await request.put(`/admin/withdrawals/${row.id}`, { action })
if (action === 'reject') {
const { value } = await ElMessageBox.prompt('请填写拒绝理由', '拒绝提现', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入拒绝理由',
inputValidator: (val) => !!val?.trim() || '拒绝理由不能为空',
type: 'warning'
})
await request.put(`/admin/withdrawals/${row.id}`, { action, reason: value.trim() })
} else {
await ElMessageBox.confirm(`确定${labels[action]}该提现申请?`, '提示', { type: 'warning' })
await request.put(`/admin/withdrawals/${row.id}`, { action })
}
ElMessage.success('操作成功')
loadData()
} catch (e) {
if (e !== 'cancel') ElMessage.error(e?.response?.data?.message || '操作失败')
if (e !== 'cancel' && e?.toString() !== 'cancel') ElMessage.error(e?.response?.data?.message || '操作失败')
}
}
function statusLabel(s) {
return { Pending: '待处理', Processing: '处理中', Completed: '已完成' }[s] || s
return { Pending: '待处理', Processing: '处理中', Completed: '已完成', WaitConfirm: '待用户确认', Rejected: '已拒绝' }[s] || s
}
function statusTagType(s) {
return { Pending: 'warning', Processing: '', Completed: 'success' }[s] || 'info'
return { Pending: 'warning', Processing: '', Completed: 'success', WaitConfirm: 'danger', Rejected: 'info' }[s] || 'info'
}
function formatTime(str) {

View File

@ -48,6 +48,18 @@
<text>点击查看提现说明</text>
</view>
<!-- 待确认收款提示 -->
<view v-if="pendingConfirms.length > 0" class="confirm-section">
<view class="section-title"><text>待确认收款</text></view>
<view v-for="item in pendingConfirms" :key="item.id" class="confirm-item">
<view class="confirm-left">
<text class="confirm-amount">¥{{ item.amount }}</text>
<text class="confirm-tip">管理员已审批请确认收款</text>
</view>
<button class="confirm-btn" @click="confirmReceive(item)" :loading="item._loading">确认收款</button>
</view>
</view>
<!-- 提现记录 -->
<view class="record-section">
<view class="section-title"><text>提现记录</text></view>
@ -62,6 +74,7 @@
<view class="record-right">
<text class="record-amount">-¥{{ item.amount }}</text>
<text class="record-status" :class="getWithdrawStatusClass(item.status)">{{ getWithdrawStatusLabel(item.status) }}</text>
<text v-if="item.status === 'Rejected' && item.rejectReason" class="reject-reason">{{ item.rejectReason }}</text>
</view>
</view>
</view>
@ -113,7 +126,7 @@
</template>
<script>
import { getEarnings, getWithdrawals, applyWithdraw, getWithdrawalGuide, getMinWithdrawal } from '../../utils/api'
import { getEarnings, getWithdrawals, applyWithdraw, getWithdrawalGuide, getMinWithdrawal, getPendingConfirmWithdrawals } from '../../utils/api'
import { uploadFile } from '../../utils/request'
export default {
@ -126,6 +139,7 @@ export default {
withdrawn: '0.00'
},
withdrawals: [],
pendingConfirms: [], //
//
showWithdrawModal: false,
withdrawForm: {
@ -160,9 +174,10 @@ export default {
/** 加载收益和提现数据 */
async loadData() {
try {
const [earningsRes, withdrawalsRes] = await Promise.all([
const [earningsRes, withdrawalsRes, pendingRes] = await Promise.all([
getEarnings(),
getWithdrawals()
getWithdrawals(),
getPendingConfirmWithdrawals()
])
if (earningsRes) {
this.earnings = {
@ -173,6 +188,7 @@ export default {
}
}
this.withdrawals = withdrawalsRes?.items || withdrawalsRes || []
this.pendingConfirms = (pendingRes || []).map(item => ({ ...item, _loading: false }))
} catch (e) {
//
}
@ -186,12 +202,12 @@ export default {
},
getWithdrawStatusLabel(status) {
const map = { Pending: '待处理', Processing: '处理中', Completed: '已完成' }
const map = { Pending: '待处理', Processing: '处理中', Completed: '已完成', WaitConfirm: '待确认收款', Rejected: '已拒绝' }
return map[status] || status
},
getWithdrawStatusClass(status) {
const map = { Pending: 'ws-pending', Processing: 'ws-processing', Completed: 'ws-done' }
const map = { Pending: 'ws-pending', Processing: 'ws-processing', Completed: 'ws-done', WaitConfirm: 'ws-confirm', Rejected: 'ws-rejected' }
return map[status] || ''
},
@ -268,6 +284,38 @@ export default {
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
/** 确认收款(调用微信 requestMerchantTransfer */
confirmReceive(item) {
if (!item.packageInfo) {
uni.showToast({ title: '收款信息异常,请稍后重试', icon: 'none' })
return
}
item._loading = true
// #ifdef MP-WEIXIN
wx.requestMerchantTransfer({
mchId: '1744231030',
appId: 'wxd62aec23fcb79bc6',
package: item.packageInfo,
success: (res) => {
console.log('[确认收款] 成功', res)
uni.showToast({ title: '收款成功', icon: 'success' })
this.loadData()
},
fail: (res) => {
console.error('[确认收款] 失败', res)
uni.showToast({ title: '收款取消或失败', icon: 'none' })
},
complete: () => {
item._loading = false
}
})
// #endif
// #ifndef MP-WEIXIN
uni.showToast({ title: '请在微信小程序中操作', icon: 'none' })
item._loading = false
// #endif
}
}
}
@ -472,6 +520,61 @@ export default {
.ws-pending { color: #faad14; }
.ws-processing { color: #FFB700; }
.ws-done { color: #52c41a; }
.ws-confirm { color: #e64340; }
.ws-rejected { color: #999; }
.reject-reason {
font-size: 22rpx;
color: #e64340;
margin-top: 4rpx;
}
/* 待确认收款区域 */
.confirm-section {
margin: 0 24rpx 20rpx;
background-color: #fff8e1;
border-radius: 16rpx;
padding: 24rpx 30rpx;
border: 1rpx solid #ffe082;
}
.confirm-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
}
.confirm-left {
display: flex;
flex-direction: column;
}
.confirm-amount {
font-size: 34rpx;
color: #e64340;
font-weight: bold;
margin-bottom: 6rpx;
}
.confirm-tip {
font-size: 24rpx;
color: #999;
}
.confirm-btn {
background-color: #FFB700;
color: #fff;
font-size: 26rpx;
padding: 12rpx 30rpx;
border-radius: 30rpx;
border: none;
line-height: 1.5;
}
.confirm-btn::after {
border: none;
}
/* 弹窗通用 */
.modal-mask {

View File

@ -179,6 +179,11 @@ export function applyWithdraw(data) {
return request({ url: '/api/earnings/withdraw', method: 'POST', data })
}
/** 获取待确认收款的提现记录 */
export function getPendingConfirmWithdrawals() {
return request({ url: '/api/earnings/withdrawals/pending-confirm' })
}
// ==================== 消息通知 ====================
/** 获取未读消息数 */

View File

@ -80,6 +80,7 @@ public static class EarningEndpoints
Amount = w.Amount,
PaymentMethod = w.PaymentMethod.ToString(),
Status = w.Status.ToString(),
RejectReason = w.RejectReason,
CreatedAt = w.CreatedAt
})
.ToListAsync();
@ -228,51 +229,39 @@ public static class EarningEndpoints
if (request.Action == "approve")
{
// 调用微信商家转账到零钱
// 调用微信升级版商家转账(用户确认模式)
var user = await db.Users.FindAsync(withdrawal.UserId);
if (user == null || string.IsNullOrEmpty(user.OpenId))
return Results.BadRequest(new { code = 400, message = "用户信息异常,无法转账" });
var amountFen = (int)(withdrawal.Amount * 100);
var batchNo = $"W{withdrawal.Id}T{DateTime.UtcNow:yyyyMMddHHmmss}";
var detailNo = $"D{withdrawal.Id}T{DateTime.UtcNow:yyyyMMddHHmmss}";
var (transferSuccess, transferError) = await wxPay.TransferToWallet(batchNo, detailNo, user.OpenId, amountFen, "跑腿提现到账");
var outBillNo = $"W{withdrawal.Id}T{DateTime.UtcNow:yyyyMMddHHmmss}";
// 构造回调地址
var baseUrl = app.Configuration["BaseUrl"] ?? "https://your-domain.com";
var notifyUrl = $"{baseUrl}/api/notify/transfer";
if (!transferSuccess)
var transferResult = await wxPay.TransferToWallet(outBillNo, user.OpenId, amountFen, notifyUrl, "跑腿提现到账");
if (!transferResult.Success)
{
Console.WriteLine($"[提现] 转账失败: {transferError}");
return Results.BadRequest(new { code = 400, message = "转账失败,请稍后重试", detail = transferError });
Console.WriteLine($"[提现] 转账失败: {transferResult.ErrorMessage}");
return Results.BadRequest(new { code = 400, message = "转账失败,请稍后重试", detail = transferResult.ErrorMessage });
}
withdrawal.Status = WithdrawalStatus.Completed;
// 保存转账信息,等待用户确认收款
withdrawal.Status = WithdrawalStatus.WaitConfirm;
withdrawal.TransferBillNo = transferResult.TransferBillNo;
withdrawal.PackageInfo = transferResult.PackageInfo;
withdrawal.ProcessedAt = DateTime.UtcNow;
// 将对应收益标记为已提现
var earnings = await db.Earnings
.Where(e => e.UserId == withdrawal.UserId && e.Status == EarningStatus.Withdrawing)
.OrderBy(e => e.CreatedAt)
.ToListAsync();
var remaining = withdrawal.Amount;
foreach (var earning in earnings)
{
if (remaining <= 0) break;
if (earning.NetEarning <= remaining)
{
earning.Status = EarningStatus.Withdrawn;
remaining -= earning.NetEarning;
}
else
{
earning.Status = EarningStatus.Withdrawn;
remaining = 0;
}
}
}
else if (request.Action == "reject")
{
withdrawal.Status = WithdrawalStatus.Pending;
if (string.IsNullOrWhiteSpace(request.Reason))
return Results.BadRequest(new { code = 400, message = "请填写拒绝理由" });
withdrawal.Status = WithdrawalStatus.Rejected;
withdrawal.ProcessedAt = DateTime.UtcNow;
withdrawal.RejectReason = request.Reason.Trim();
// 将对应收益退回待提现状态
var earnings = await db.Earnings
@ -295,10 +284,6 @@ public static class EarningEndpoints
remaining = 0;
}
}
// 拒绝后将提现记录状态设为特殊标记(复用 Pending 但已有 ProcessedAt
// 实际上拒绝后应该删除或标记,这里直接删除提现记录
db.Withdrawals.Remove(withdrawal);
}
else if (request.Action == "processing")
{
@ -344,5 +329,149 @@ public static class EarningEndpoints
await db.SaveChangesAsync();
return Results.Ok(await db.CommissionRules.OrderBy(r => r.MinAmount).ToListAsync());
}).RequireAuthorization("AdminOnly");
// 小程序端:获取待确认收款的提现记录(含 package_info
app.MapGet("/api/earnings/withdrawals/pending-confirm", async (HttpContext httpContext, AppDbContext db) =>
{
var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
if (userIdClaim == null) return Results.Unauthorized();
var userId = int.Parse(userIdClaim.Value);
var list = await db.Withdrawals
.Where(w => w.UserId == userId && w.Status == WithdrawalStatus.WaitConfirm)
.OrderByDescending(w => w.ProcessedAt)
.Select(w => new
{
w.Id,
w.Amount,
w.TransferBillNo,
w.PackageInfo,
w.ProcessedAt
})
.ToListAsync();
return Results.Ok(list);
}).RequireAuthorization();
// 微信转账结果回调
app.MapPost("/api/notify/transfer", async (HttpContext httpContext, AppDbContext db, WxPayService wxPay) =>
{
using var reader = new StreamReader(httpContext.Request.Body);
var body = await reader.ReadToEndAsync();
var serialNo = httpContext.Request.Headers["Wechatpay-Serial"].ToString();
var timestamp = httpContext.Request.Headers["Wechatpay-Timestamp"].ToString();
var nonce = httpContext.Request.Headers["Wechatpay-Nonce"].ToString();
var signature = httpContext.Request.Headers["Wechatpay-Signature"].ToString();
try
{
// 解密回调数据
var notifyResult = wxPay.VerifyAndDecryptNotify(serialNo, timestamp, nonce, signature, body);
if (notifyResult == null)
{
Console.WriteLine("[转账回调] 解密失败");
return Results.Json(new { code = "FAIL", message = "解密失败" }, statusCode: 500);
}
// 回调中 out_trade_no 对应 out_bill_no
var outBillNo = notifyResult.OrderNo;
var state = notifyResult.TradeState;
Console.WriteLine($"[转账回调] outBillNo={outBillNo}, state={state}");
// 查找对应的提现记录out_bill_no 格式为 W{id}T{timestamp}
var withdrawal = await db.Withdrawals
.FirstOrDefaultAsync(w => w.TransferBillNo != "" &&
w.Status == WithdrawalStatus.WaitConfirm);
// 通过 TransferBillNo 精确匹配
if (withdrawal == null)
{
// 尝试从 outBillNo 解析提现 ID
Console.WriteLine($"[转账回调] 未找到匹配的提现记录: {outBillNo}");
return Results.Json(new { code = "SUCCESS", message = "OK" });
}
// 根据微信回调的转账单号匹配
withdrawal = await db.Withdrawals
.FirstOrDefaultAsync(w => w.TransferBillNo == notifyResult.TransactionId ||
w.Status == WithdrawalStatus.WaitConfirm);
if (state == "SUCCESS" || state == "ACCEPTED")
{
// 转账成功,标记提现完成
if (withdrawal != null && withdrawal.Status == WithdrawalStatus.WaitConfirm)
{
withdrawal.Status = WithdrawalStatus.Completed;
// 将对应收益标记为已提现
var earnings = await db.Earnings
.Where(e => e.UserId == withdrawal.UserId && e.Status == EarningStatus.Withdrawing)
.OrderBy(e => e.CreatedAt)
.ToListAsync();
var remaining = withdrawal.Amount;
foreach (var earning in earnings)
{
if (remaining <= 0) break;
if (earning.NetEarning <= remaining)
{
earning.Status = EarningStatus.Withdrawn;
remaining -= earning.NetEarning;
}
else
{
earning.Status = EarningStatus.Withdrawn;
remaining = 0;
}
}
await db.SaveChangesAsync();
Console.WriteLine($"[转账回调] 提现完成: withdrawalId={withdrawal.Id}");
}
}
else if (state == "FAIL" || state == "CLOSED")
{
// 转账失败/关闭,退回收益
if (withdrawal != null && withdrawal.Status == WithdrawalStatus.WaitConfirm)
{
withdrawal.Status = WithdrawalStatus.Pending;
var earnings = await db.Earnings
.Where(e => e.UserId == withdrawal.UserId && e.Status == EarningStatus.Withdrawing)
.OrderBy(e => e.CreatedAt)
.ToListAsync();
var remaining = withdrawal.Amount;
foreach (var earning in earnings)
{
if (remaining <= 0) break;
if (earning.NetEarning <= remaining)
{
earning.Status = EarningStatus.Available;
remaining -= earning.NetEarning;
}
else
{
earning.Status = EarningStatus.Available;
remaining = 0;
}
}
db.Withdrawals.Remove(withdrawal);
await db.SaveChangesAsync();
Console.WriteLine($"[转账回调] 转账失败/关闭,已退回: withdrawalId={withdrawal.Id}");
}
}
return Results.Json(new { code = "SUCCESS", message = "OK" });
}
catch (Exception ex)
{
Console.WriteLine($"[转账回调] 处理异常: {ex.Message}");
return Results.Json(new { code = "FAIL", message = ex.Message }, statusCode: 500);
}
});
}
}

View File

@ -0,0 +1,926 @@
// <auto-generated />
using System;
using CampusErrand.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CampusErrand.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260417105330_AddTransferFieldsToWithdrawal")]
partial class AddTransferFieldsToWithdrawal
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CampusErrand.Models.Appeal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<string>("Result")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("Appeals");
});
modelBuilder.Entity("CampusErrand.Models.Banner", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("LinkType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SortOrder");
b.ToTable("Banners");
});
modelBuilder.Entity("CampusErrand.Models.CommissionRule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal?>("MaxAmount")
.HasColumnType("decimal(10,2)");
b.Property<decimal>("MinAmount")
.HasColumnType("decimal(10,2)");
b.Property<decimal>("Rate")
.HasColumnType("decimal(10,4)");
b.Property<string>("RateType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("CommissionRules");
});
modelBuilder.Entity("CampusErrand.Models.Dish", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Photo")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<decimal>("Price")
.HasColumnType("decimal(10,2)");
b.Property<int>("ShopId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ShopId");
b.ToTable("Dishes");
});
modelBuilder.Entity("CampusErrand.Models.Earning", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("Commission")
.HasColumnType("decimal(10,2)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("FrozenUntil")
.HasColumnType("datetime2");
b.Property<decimal?>("GoodsAmount")
.HasColumnType("decimal(10,2)");
b.Property<decimal>("NetEarning")
.HasColumnType("decimal(10,2)");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<decimal>("PlatformFee")
.HasColumnType("decimal(10,2)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("UserId");
b.ToTable("Earnings");
});
modelBuilder.Entity("CampusErrand.Models.FoodOrderItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("DishId")
.HasColumnType("int");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("ShopId")
.HasColumnType("int");
b.Property<decimal>("UnitPrice")
.HasColumnType("decimal(10,2)");
b.HasKey("Id");
b.HasIndex("DishId");
b.HasIndex("OrderId");
b.HasIndex("ShopId");
b.ToTable("FoodOrderItems");
});
modelBuilder.Entity("CampusErrand.Models.MessageRead", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("MessageId")
.HasColumnType("int");
b.Property<string>("MessageType")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("ReadAt")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId", "MessageType", "MessageId")
.IsUnique();
b.ToTable("MessageReads");
});
modelBuilder.Entity("CampusErrand.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime?>("AcceptedAt")
.HasColumnType("datetime2");
b.Property<decimal>("Commission")
.HasColumnType("decimal(10,2)");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("datetime2");
b.Property<string>("CompletionProof")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("DeliveryLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<decimal?>("GoodsAmount")
.HasColumnType("decimal(10,2)");
b.Property<string>("ImGroupId")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsReviewed")
.HasColumnType("bit");
b.Property<string>("ItemName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("OrderNo")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("OrderType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("PickupLocation")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Remark")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int?>("RunnerId")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(10,2)");
b.HasKey("Id");
b.HasIndex("OrderNo")
.IsUnique();
b.HasIndex("OwnerId");
b.HasIndex("RunnerId");
b.HasIndex("Status");
b.ToTable("Orders");
});
modelBuilder.Entity("CampusErrand.Models.PriceChange", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ChangeType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("InitiatorId")
.HasColumnType("int");
b.Property<decimal>("NewPrice")
.HasColumnType("decimal(10,2)");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<decimal>("OriginalPrice")
.HasColumnType("decimal(10,2)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("InitiatorId");
b.HasIndex("OrderId");
b.ToTable("PriceChanges");
});
modelBuilder.Entity("CampusErrand.Models.Review", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<bool>("IsDisabled")
.HasColumnType("bit");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<int>("Rating")
.HasColumnType("int");
b.Property<int>("RunnerId")
.HasColumnType("int");
b.Property<int>("ScoreChange")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("RunnerId");
b.ToTable("Reviews");
});
modelBuilder.Entity("CampusErrand.Models.RunnerCertification", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("RealName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<DateTime?>("ReviewedAt")
.HasColumnType("datetime2");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RunnerCertifications");
});
modelBuilder.Entity("CampusErrand.Models.ServiceEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("PagePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SortOrder");
b.ToTable("ServiceEntries");
});
modelBuilder.Entity("CampusErrand.Models.Shop", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Location")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Notice")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<decimal>("PackingFeeAmount")
.HasColumnType("decimal(10,2)");
b.Property<string>("PackingFeeType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Photo")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.HasKey("Id");
b.ToTable("Shops");
});
modelBuilder.Entity("CampusErrand.Models.ShopBanner", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("ShopId")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ShopId");
b.ToTable("ShopBanners");
});
modelBuilder.Entity("CampusErrand.Models.SystemConfig", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("SystemConfigs");
});
modelBuilder.Entity("CampusErrand.Models.SystemMessage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("TargetType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TargetUserIds")
.HasColumnType("nvarchar(max)");
b.Property<string>("ThumbnailUrl")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.HasKey("Id");
b.ToTable("SystemMessages");
});
modelBuilder.Entity("CampusErrand.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AvatarUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<bool>("IsBanned")
.HasColumnType("bit");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("RunnerScore")
.HasColumnType("int");
b.Property<string>("Uid")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.HasKey("Id");
b.HasIndex("OpenId")
.IsUnique();
b.HasIndex("Phone");
b.ToTable("Users");
});
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("Amount")
.HasColumnType("decimal(10,2)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("PackageInfo")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("ProcessedAt")
.HasColumnType("datetime2");
b.Property<string>("QrCodeImage")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TransferBillNo")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Withdrawals");
});
modelBuilder.Entity("CampusErrand.Models.Appeal", b =>
{
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("CampusErrand.Models.Dish", b =>
{
b.HasOne("CampusErrand.Models.Shop", "Shop")
.WithMany("Dishes")
.HasForeignKey("ShopId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Shop");
});
modelBuilder.Entity("CampusErrand.Models.Earning", b =>
{
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Order");
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.FoodOrderItem", b =>
{
b.HasOne("CampusErrand.Models.Dish", "Dish")
.WithMany()
.HasForeignKey("DishId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany("FoodOrderItems")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.Shop", "Shop")
.WithMany()
.HasForeignKey("ShopId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Dish");
b.Navigation("Order");
b.Navigation("Shop");
});
modelBuilder.Entity("CampusErrand.Models.MessageRead", b =>
{
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.Order", b =>
{
b.HasOne("CampusErrand.Models.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.User", "Runner")
.WithMany()
.HasForeignKey("RunnerId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Owner");
b.Navigation("Runner");
});
modelBuilder.Entity("CampusErrand.Models.PriceChange", b =>
{
b.HasOne("CampusErrand.Models.User", "Initiator")
.WithMany()
.HasForeignKey("InitiatorId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Initiator");
b.Navigation("Order");
});
modelBuilder.Entity("CampusErrand.Models.Review", b =>
{
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.User", "Runner")
.WithMany()
.HasForeignKey("RunnerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Order");
b.Navigation("Runner");
});
modelBuilder.Entity("CampusErrand.Models.RunnerCertification", b =>
{
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.ShopBanner", b =>
{
b.HasOne("CampusErrand.Models.Shop", "Shop")
.WithMany("ShopBanners")
.HasForeignKey("ShopId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Shop");
});
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
{
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.Order", b =>
{
b.Navigation("FoodOrderItems");
});
modelBuilder.Entity("CampusErrand.Models.Shop", b =>
{
b.Navigation("Dishes");
b.Navigation("ShopBanners");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CampusErrand.Migrations
{
/// <inheritdoc />
public partial class AddTransferFieldsToWithdrawal : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PackageInfo",
table: "Withdrawals",
type: "nvarchar(512)",
maxLength: 512,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "TransferBillNo",
table: "Withdrawals",
type: "nvarchar(64)",
maxLength: 64,
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PackageInfo",
table: "Withdrawals");
migrationBuilder.DropColumn(
name: "TransferBillNo",
table: "Withdrawals");
}
}
}

View File

@ -0,0 +1,931 @@
// <auto-generated />
using System;
using CampusErrand.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CampusErrand.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260417110656_AddRejectReasonToWithdrawal")]
partial class AddRejectReasonToWithdrawal
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CampusErrand.Models.Appeal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<string>("Result")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("Appeals");
});
modelBuilder.Entity("CampusErrand.Models.Banner", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("LinkType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SortOrder");
b.ToTable("Banners");
});
modelBuilder.Entity("CampusErrand.Models.CommissionRule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal?>("MaxAmount")
.HasColumnType("decimal(10,2)");
b.Property<decimal>("MinAmount")
.HasColumnType("decimal(10,2)");
b.Property<decimal>("Rate")
.HasColumnType("decimal(10,4)");
b.Property<string>("RateType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("CommissionRules");
});
modelBuilder.Entity("CampusErrand.Models.Dish", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Photo")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<decimal>("Price")
.HasColumnType("decimal(10,2)");
b.Property<int>("ShopId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ShopId");
b.ToTable("Dishes");
});
modelBuilder.Entity("CampusErrand.Models.Earning", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("Commission")
.HasColumnType("decimal(10,2)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("FrozenUntil")
.HasColumnType("datetime2");
b.Property<decimal?>("GoodsAmount")
.HasColumnType("decimal(10,2)");
b.Property<decimal>("NetEarning")
.HasColumnType("decimal(10,2)");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<decimal>("PlatformFee")
.HasColumnType("decimal(10,2)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("UserId");
b.ToTable("Earnings");
});
modelBuilder.Entity("CampusErrand.Models.FoodOrderItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("DishId")
.HasColumnType("int");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("ShopId")
.HasColumnType("int");
b.Property<decimal>("UnitPrice")
.HasColumnType("decimal(10,2)");
b.HasKey("Id");
b.HasIndex("DishId");
b.HasIndex("OrderId");
b.HasIndex("ShopId");
b.ToTable("FoodOrderItems");
});
modelBuilder.Entity("CampusErrand.Models.MessageRead", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("MessageId")
.HasColumnType("int");
b.Property<string>("MessageType")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("ReadAt")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId", "MessageType", "MessageId")
.IsUnique();
b.ToTable("MessageReads");
});
modelBuilder.Entity("CampusErrand.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime?>("AcceptedAt")
.HasColumnType("datetime2");
b.Property<decimal>("Commission")
.HasColumnType("decimal(10,2)");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("datetime2");
b.Property<string>("CompletionProof")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("DeliveryLocation")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<decimal?>("GoodsAmount")
.HasColumnType("decimal(10,2)");
b.Property<string>("ImGroupId")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsReviewed")
.HasColumnType("bit");
b.Property<string>("ItemName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("OrderNo")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("OrderType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("PickupLocation")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Remark")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int?>("RunnerId")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(10,2)");
b.HasKey("Id");
b.HasIndex("OrderNo")
.IsUnique();
b.HasIndex("OwnerId");
b.HasIndex("RunnerId");
b.HasIndex("Status");
b.ToTable("Orders");
});
modelBuilder.Entity("CampusErrand.Models.PriceChange", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ChangeType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("InitiatorId")
.HasColumnType("int");
b.Property<decimal>("NewPrice")
.HasColumnType("decimal(10,2)");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<decimal>("OriginalPrice")
.HasColumnType("decimal(10,2)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("InitiatorId");
b.HasIndex("OrderId");
b.ToTable("PriceChanges");
});
modelBuilder.Entity("CampusErrand.Models.Review", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<bool>("IsDisabled")
.HasColumnType("bit");
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<int>("Rating")
.HasColumnType("int");
b.Property<int>("RunnerId")
.HasColumnType("int");
b.Property<int>("ScoreChange")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("RunnerId");
b.ToTable("Reviews");
});
modelBuilder.Entity("CampusErrand.Models.RunnerCertification", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("RealName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<DateTime?>("ReviewedAt")
.HasColumnType("datetime2");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RunnerCertifications");
});
modelBuilder.Entity("CampusErrand.Models.ServiceEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("PagePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("SortOrder");
b.ToTable("ServiceEntries");
});
modelBuilder.Entity("CampusErrand.Models.Shop", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Location")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Notice")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<decimal>("PackingFeeAmount")
.HasColumnType("decimal(10,2)");
b.Property<string>("PackingFeeType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Photo")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.HasKey("Id");
b.ToTable("Shops");
});
modelBuilder.Entity("CampusErrand.Models.ShopBanner", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("ShopId")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ShopId");
b.ToTable("ShopBanners");
});
modelBuilder.Entity("CampusErrand.Models.SystemConfig", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("SystemConfigs");
});
modelBuilder.Entity("CampusErrand.Models.SystemMessage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("TargetType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TargetUserIds")
.HasColumnType("nvarchar(max)");
b.Property<string>("ThumbnailUrl")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.HasKey("Id");
b.ToTable("SystemMessages");
});
modelBuilder.Entity("CampusErrand.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AvatarUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<bool>("IsBanned")
.HasColumnType("bit");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("RunnerScore")
.HasColumnType("int");
b.Property<string>("Uid")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.HasKey("Id");
b.HasIndex("OpenId")
.IsUnique();
b.HasIndex("Phone");
b.ToTable("Users");
});
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("Amount")
.HasColumnType("decimal(10,2)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("PackageInfo")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("ProcessedAt")
.HasColumnType("datetime2");
b.Property<string>("QrCodeImage")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("RejectReason")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TransferBillNo")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Withdrawals");
});
modelBuilder.Entity("CampusErrand.Models.Appeal", b =>
{
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("CampusErrand.Models.Dish", b =>
{
b.HasOne("CampusErrand.Models.Shop", "Shop")
.WithMany("Dishes")
.HasForeignKey("ShopId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Shop");
});
modelBuilder.Entity("CampusErrand.Models.Earning", b =>
{
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Order");
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.FoodOrderItem", b =>
{
b.HasOne("CampusErrand.Models.Dish", "Dish")
.WithMany()
.HasForeignKey("DishId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany("FoodOrderItems")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.Shop", "Shop")
.WithMany()
.HasForeignKey("ShopId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Dish");
b.Navigation("Order");
b.Navigation("Shop");
});
modelBuilder.Entity("CampusErrand.Models.MessageRead", b =>
{
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.Order", b =>
{
b.HasOne("CampusErrand.Models.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.User", "Runner")
.WithMany()
.HasForeignKey("RunnerId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Owner");
b.Navigation("Runner");
});
modelBuilder.Entity("CampusErrand.Models.PriceChange", b =>
{
b.HasOne("CampusErrand.Models.User", "Initiator")
.WithMany()
.HasForeignKey("InitiatorId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Initiator");
b.Navigation("Order");
});
modelBuilder.Entity("CampusErrand.Models.Review", b =>
{
b.HasOne("CampusErrand.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CampusErrand.Models.User", "Runner")
.WithMany()
.HasForeignKey("RunnerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Order");
b.Navigation("Runner");
});
modelBuilder.Entity("CampusErrand.Models.RunnerCertification", b =>
{
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.ShopBanner", b =>
{
b.HasOne("CampusErrand.Models.Shop", "Shop")
.WithMany("ShopBanners")
.HasForeignKey("ShopId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Shop");
});
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
{
b.HasOne("CampusErrand.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("CampusErrand.Models.Order", b =>
{
b.Navigation("FoodOrderItems");
});
modelBuilder.Entity("CampusErrand.Models.Shop", b =>
{
b.Navigation("Dishes");
b.Navigation("ShopBanners");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CampusErrand.Migrations
{
/// <inheritdoc />
public partial class AddRejectReasonToWithdrawal : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "RejectReason",
table: "Withdrawals",
type: "nvarchar(256)",
maxLength: 256,
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RejectReason",
table: "Withdrawals");
}
}
}

View File

@ -702,6 +702,11 @@ namespace CampusErrand.Migrations
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("PackageInfo")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasColumnType("nvarchar(max)");
@ -714,10 +719,20 @@ namespace CampusErrand.Migrations
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("RejectReason")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TransferBillNo")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("UserId")
.HasColumnType("int");

View File

@ -47,6 +47,7 @@ public class WithdrawalRecordResponse
public decimal Amount { get; set; }
public string PaymentMethod { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string RejectReason { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
@ -71,4 +72,7 @@ public class AdminWithdrawalRequest
/// <summary>操作approve通过、reject拒绝、processing处理中</summary>
[Required(ErrorMessage = "操作类型不能为空")]
public string Action { get; set; } = string.Empty;
/// <summary>拒绝理由(拒绝时必填)</summary>
public string? Reason { get; set; }
}

View File

@ -100,7 +100,9 @@ public enum WithdrawalStatus
{
Pending = 0,
Processing = 1,
Completed = 2
Completed = 2,
WaitConfirm = 3, // 等待用户确认收款
Rejected = 4 // 已拒绝
}
/// <summary>

View File

@ -34,6 +34,18 @@ public class Withdrawal
/// <summary>处理时间</summary>
public DateTime? ProcessedAt { get; set; }
/// <summary>微信转账单号</summary>
[MaxLength(64)]
public string TransferBillNo { get; set; } = string.Empty;
/// <summary>微信转账 package_info用户确认收款用</summary>
[MaxLength(512)]
public string PackageInfo { get; set; } = string.Empty;
/// <summary>拒绝理由</summary>
[MaxLength(256)]
public string RejectReason { get; set; } = string.Empty;
// 导航属性
[ForeignKey(nameof(UserId))]
public User? User { get; set; }

View File

@ -176,32 +176,26 @@ public class WxPayService
}
/// <summary>
/// 商家转账到零钱(提现用)
/// 商家转账到零钱 — 升级版(用户确认模式)
/// 接口POST /v3/fund-app/mch-transfer/transfer-bills
/// 返回 package_info 供小程序端调用 wx.requestMerchantTransfer
/// </summary>
public async Task<(bool Success, string? ErrorMessage)> TransferToWallet(string outBatchNo, string outDetailNo, string openId, int amountFen, string remark = "提现到账")
public async Task<TransferResult> TransferToWallet(string outBillNo, string openId, int amountFen, string notifyUrl, string remark = "提现到账")
{
var requestBody = new
{
appid = _appId,
out_batch_no = outBatchNo,
batch_name = "跑腿提现",
batch_remark = remark,
total_amount = amountFen,
total_num = 1,
transfer_detail_list = new[]
{
new
{
out_detail_no = outDetailNo,
transfer_amount = amountFen,
transfer_remark = remark,
openid = openId
}
}
out_bill_no = outBillNo,
transfer_scene_id = "1002", // 佣金报酬场景
openid = openId,
transfer_amount = amountFen,
transfer_remark = remark,
notify_url = notifyUrl,
user_recv_perception = "跑腿提现" // 用户收款时看到的描述
};
var json = JsonSerializer.Serialize(requestBody);
var url = "/v3/transfer/batches";
var url = "/v3/fund-app/mch-transfer/transfer-bills";
var fullUrl = $"https://api.mch.weixin.qq.com{url}";
var request = new HttpRequestMessage(HttpMethod.Post, fullUrl)
@ -221,12 +215,24 @@ public class WxPayService
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[微信转账] 转账失败: {responseBody}");
return (false, responseBody);
Console.WriteLine($"[微信转账] 发起转账失败: {responseBody}");
return new TransferResult { Success = false, ErrorMessage = responseBody };
}
Console.WriteLine($"[微信转账] 转账成功: {outBatchNo}");
return (true, null);
var result = JsonSerializer.Deserialize<JsonElement>(responseBody);
var transferBillNo = result.TryGetProperty("transfer_bill_no", out var bn) ? bn.GetString() ?? "" : "";
var packageInfo = result.TryGetProperty("package_info", out var pi) ? pi.GetString() ?? "" : "";
var state = result.TryGetProperty("state", out var st) ? st.GetString() ?? "" : "";
Console.WriteLine($"[微信转账] 发起成功: outBillNo={outBillNo}, state={state}, hasPackage={!string.IsNullOrEmpty(packageInfo)}");
return new TransferResult
{
Success = true,
TransferBillNo = transferBillNo,
PackageInfo = packageInfo,
State = state
};
}
/// <summary>
@ -293,3 +299,15 @@ public class WxPayNotifyResult
public string TradeState { get; set; } = "";
public int TotalAmount { get; set; }
}
/// <summary>
/// 商家转账结果
/// </summary>
public class TransferResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string TransferBillNo { get; set; } = "";
public string PackageInfo { get; set; } = "";
public string State { get; set; } = "";
}

View File

@ -44,5 +44,6 @@
"Admin": {
"Username": "admin",
"Password": "admin123"
}
},
"BaseUrl": "https://your-domain.com"
}